📜 ⬆️ ⬇️

Momentarily faster: we measure the time of packing and unpacking of significant data types

Good day, Habr!



Many inexperienced developers do not always know and understand what happens behind the scenes of their code. Now we will talk about the packaging and unpacking of significant data types (in Russian, it sounds awful, so "boxing and unboxing value types").
Under the cut is a small example and measurement of the runtime.

What is boxing?
Short. There are significant data types (value types) and reference (reference types). Variables of significant data types store the value itself (thanks, cap!), Variable reference data types refer to the location in memory where this value is stored.

int valType = 15; 

This is a significant data type. The value of the valType variable will be stored in the stack. Many standard data types are meaningful (int, byte, long, bool, etc.).
Further if we try to do this:

 int valType = 15; Object refType = valType; 

And the result is a variable of reference type ( refType ). Here the following will happen: first, the value of the valType variable (significant type) appears in the stack, then a container will be created in memory to store the value of this variable (in our case, the container for the variable of type int , that is, 4 bytes for the value + sync block index (4 more bytes)), and now the pointer to this container will be stored in our variable of reference type ( refType ). This process is called boxing.
Details can be found here , and it is better to read in the book of J. Richter “CLR via C #” (Chapter 5).
')
The most unpleasant thing about these operations is that they occur implicitly.
For example, we want to display a number in the console. So:

 Console.WriteLine(20); 

Or like this:

 Console.WriteLine("{0}", 20); 

What is the difference? Let's look at the result of compilation in MSIL (this can be done with the ILdasm.exe utility):

 //    Console.WriteLine(20); IL_0000: ldc.i4.s 20 IL_0002: call void [mscorlib]System.Console::WriteLine(int32) //     Console.WriteLine("{0}", 20); IL_0007: ldstr "{0}" IL_000c: ldc.i4.s 20 IL_000e: box [mscorlib]System.Int32 IL_0013: call void [mscorlib]System.Console::WriteLine(string, object) 

In the second case, we see the box command, which performs the packaging.
To understand where it came from, take a look at the signature of the Console.WriteLine method and note that there are already 18 of them.
The first call uses the following signature:

 void WriteLine(int value); 

Everything is simple - this method takes a significant data type int , we pass a value of type int , the parameter is passed by value. Packaging is not needed here.
In the second case, another signature is used:

 void WriteLine(string format, object arg0); 

With the transfer of the format string it is clear: a string is required - we are passing the string. And with the argument arg0 a bit more complicated: the method asks for an object of the object data type object from us, and we pass a value of type int to the method. This is where packaging is needed. As a result, a container for the int type is created in memory, the value 20 is copied into it, and a pointer to this container is placed in the arg0 argument.

Now let's try to calculate if our code is slowing down these operations.
To do this, I wrote a small piece of code:

Code sheet
 static void Main() { //   ,  var val = 15; //   ,   Object obj = val; //   -  =) const int cycles = 1000000; var str = ""; //       var results = new List<TimeSpan>(); //   20 ,       for (var j = 0; j < 20; j++) { //   var start = DateTime.Now; for (var i = 0; i < cycles; i++) { //    10   //      str = String.Format("{0}, {1}, {2}, {3}, {4}, {5}, {6}, {7}, {8}, {9}", obj, obj, obj, obj, obj, obj, obj, obj, obj, obj); } //   var end = DateTime.Now; //         (box) var objResult = end.Subtract(start); //    start = DateTime.Now; for (var i = 0; i < cycles; i++) { //      //      10   (box) str = String.Format("{0}, {1}, {2}, {3}, {4}, {5}, {6}, {7}, {8}, {9}", val, val, val, val, val, val, val, val, val, val); } //   end = DateTime.Now; //     ,    10    var valResult = end.Subtract(start); //           results.Add(valResult.Subtract(objResult)); } //    foreach (var timeDif in results) { Console.WriteLine(timeDif); } //     Console.WriteLine(); Console.Write("Milliseconds need for 10KK boxing operations: "); Console.WriteLine(results.Aggregate(TimeSpan.Zero, (sum, current) => sum.Add(current)).TotalMilliseconds / results.Count); } 


The result of the execution I got is the following (Intel Core i5 750 2.67GHz processor, 4 cores, ran on one):

 00:00:00.0600060 00:00:00.0770077 00:00:00.0570057 00:00:00.0710071 00:00:00.0680068 00:00:00.0650065 00:00:00.0530053 00:00:00.0740074 00:00:00.0570057 00:00:00.0580058 00:00:00.0590059 00:00:00.0500050 00:00:00.0550055 00:00:00.0720072 00:00:00.0800080 00:00:00.0640064 00:00:00.0640064 00:00:00.0670067 00:00:00.0660066 00:00:00.0590059 Milliseconds need for 10KK boxing operations: 63,80638 

Total on average almost 64ms to 10 million. packing operations.

Conclusion


As a conclusion, I want to say that all of the above is not at all a paranoid reason to look for boxing in the code with the disassembler and to strive for an extra millimeter per second at a speed of one hundred kilometers per hour. No, of course, this is complete nonsense. But to understand what is actually happening in your code is important. And at some point, an extra operation in a cycle performed billions of times can become critical.

UPD! In the comments, the user exmachine suggests that I was not quite correct measurements. Here are the results as amended:
results
 Cache warming results: 00:00:00.0505219 00:00:00.0491484 00:00:00.0527804 00:00:00.0586028 00:00:00.0595744 00:00:00.0573599 00:00:00.0678498 00:00:00.0560197 00:00:00.0591139 00:00:00.0382205 00:00:00.0602378 00:00:00.0862110 00:00:00.0632895 00:00:00.0584091 00:00:00.0556713 00:00:00.0572194 00:00:00.0544349 00:00:00.0750407 00:00:00.0579586 00:00:00.0561487 Test results: 00:00:00.0640218 00:00:00.0558972 00:00:00.0612732 00:00:00.0560300 00:00:00.0547193 00:00:00.0556158 00:00:00.0558210 00:00:00.0554421 00:00:00.0632168 00:00:00.0611355 00:00:00.0539173 00:00:00.0594863 00:00:00.0549896 00:00:00.0585462 00:00:00.0598485 00:00:00.0586522 00:00:00.0560457 00:00:00.0568806 00:00:00.0784523 00:00:00.0521756 Milliseconds need for 10KK boxing operations: 58,60835 



UPD2! The user mstyura reminded that earlier on Habré a similar question was already lit. I advise you to look there for more information.

Source: https://habr.com/ru/post/210108/


All Articles