Chapter 9 discusses some widely used real-world abstract machines, and in particular the Java Virtual Machine and the .NET virtual machine. We present JVM and .NET bytecode which are the languages generated by Java and C# compilers and which are executed by these abstract machines. We also show an example of how a program is compiled from Java to Java bytecode (by the Java compiler) and further to real machine code (by the Java virtual machine).
The folder Virtual contains the files mentioned below.
Folder Selsort contains implementations of selection sort in Java
and C#.
This will compile the C# version in Debug mode to a file
bin/Debug/net10.0/Selsort.dll:
cd Selsort
dotnet build Selsort.csprojYou can run the program with
dotnet run To disassemble Selsort.dll, you need the tool ilspycmd.
Check whether ilspycmd is already installed:
dotnet tool list -gPackage Id Version Commands
----------------------------------------
ilspycmd 10.0.1.8346 ilspycmdIf not installed, use
dotnet tool install --global ilspycmdThe tool ilspycmd is put into folder ~/.dotnet/tools and the path
should be included in your PATH environment variable.
You can disassemble the .NET bytecode of Selsort to get a file
Selsort.il containing the disassembled bytecode in text form:
ilspycmd -il bin/Debug/net10.0/Selsort.dll > Selsort.ilCompile the Java version and then disassemble its Java Virtual
Machine bytecode into text file Selsort.jvmbytecode:
javac Selsort.javajavap -verbose -c -p Selsort > Selsort.jvmbytecodeThe programs CircularQueue.java and CircularQueue.cs implement a
generic class CircularQueue<T>. Compiling and then disassembling
them to bytecode shows that the type parameters such as T have been
erased in the Java bytecode, whereas they are preserved in the .NET
bytecode, although renamed to !T or similar.
This will compile and run the C# program in folder CircularQueue:
cd CircularQueue
dotnet build CircularQueue.csprojdotnet runTo put the program's .NET bytecode in text file CircularQueue.il:
ilspycmd -il bin/Debug/net10.0/CircularQueue.dll > CircularQueue.ilCompile the Java version and disassemble its bytecode into file
CircularQueue.jvmbytecode:
javac CircularQueue.java javap -verbose -c -p CircularQueue > CircularQueue.jvmbytecodeThe programs StringConcatSpeed.java and StringConcatSpeed.cs
demonstrate how repeated string concatenation using the (+) operator
is very slow and stresses the garbage collector in both languages.
To compile and run the Java program, with verbose garbage collection messages:
cd StringConcatSpeed
javac StringConcatSpeed.javajava -verbosegc StringConcatSpeedTo compile and run the C# program in folder StringConcatSpeed:
dotnet build -c Release StringConcatSpeed.csprojdotnet runTo investigate dotnet runtime heap allocation, install and use the dotnet-counters tool:
dotnet tool install --global dotnet-countersThe use two terminal windows. In terminal window A, run StringConcatSpeed:
dotnet runwhich quickly stops with the "Press return to continue..." prompt.
In another terminal window B, run dotnet-counters to collect
standard System.Runtime counters for the StringConcatSpeed process
(name gets truncated to StringConcatSpe):
dotnet-counters collect -n StringConcatSpeBack in terminal window A, press return so that StringConcatSpeed uses
String operator "+" repeatedly to create a growing string.
Over in terminal window B, dotnet-counters has created a file
counter.csv in comma-separated format.
Use Excel or another spreadsheet program to open that file. Add
column filters with Home > Sort & Filter > Filter. In the Counter Name column, select the counter "dotnet.gc.heap.total_allocated (By / 1 sec)". This will typically show in column Mean/Increment that
the .NET runtime allocates 12-13 GB per second. In total 240 GB data
was been allocated, which is roughly 100,000 times more than the size
of the final 2.6 MB string. Repeated concatenation with "+" is
clearly a wasteful way to build strings.
Compile in Debug mode, run, and put the bytecode code in
Square-debug.il:
cd Square
dotnet build Square.csprojdotnet runilspycmd -il bin/Debug/net10.0/Square.dll > Square-debug.ilThen compile with option -c Release instead and investigate the
bytecode in Square-release.il:
dotnet build -c Release Square.csprojilspycmd -il bin/Release/net10.0/Square.dll > Square-release.ilFolder Ex26 contains programs Ex26.java and Ex26.cs with method
isPrime:
cd Ex26
dotnet build -c Release Ex26.csprojdotnet run 1000000ilspycmd -il bin/Release/net10.0/Ex26.dll > Ex26.ilTo investigate the machine code generated by the .NET runtime JIT
compiler, set the environment variable DOTNET_JitDisasm as follows:
-
Linux and MacOS in bash:
export DOTNET_JitDisasm="isPrime" -
Windows Command Prompt:
set DOTNET_JitDisasm="isPrime" -
Windows Powershell:
$env:DOTNET_JitDisasm="isPrime"
Then run the compiled Ex26.cs program:
dotnet run -c Release 10000000To compile Ex26.java and disassemble it to bytecode:
javac Ex26.javajava Ex26 1000000javap -c -p Ex26 > Ex26.jvmbytecodeTo investigate the machine code generated by the JVM JIT compiler. To
get readable symbolic machine code you need the Hotspot disassembler
dynamic library, typically called hsdis-aarch64.dylib or the like.
Then run:
java -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly Ex26 10000000This will produce a large amount of text output. You can look for the
machine code corresponding for method isPrime by searching for
Ex26::isPrime
cd Ex6
javac ex6java.javajava ex6javajavap -c -p LinkedListSimilar to Ex26.java above.