⬆️ ⬇️

Is R # right: call to .ToString () is redundant?

This post is published at the request of mstyura habrauser , which does not have enough karma to publish. If you liked the article, thank the author and help him with karma.


I want to share with the Habrosocommunity the result of my research on packaging / unpacking significant types. Two things led me to write this topic: Richter 's book “CLR via c #” and R # itself . The latter, in my opinion, gave "dishonest" comments to my code.



What is the matter?



The fact is that I wrote a fairly standard code, like this one

string str = "habrahabr" ;

int val = 0;

var resultString = str + val.ToString();


* This source code was highlighted with Source Code Highlighter .


Resharper did not like my explicit call to the ToString () method of the val variable, but I naturally didn’t like what it tells me what to do when I know for sure that in this case it’s better to do what I wrote. Let's see who is right. To begin, I propose to figure out what operations will occur when executing the code proposed by R #, that is, this

string str = "habrahabr" ;

int val = 0;

var resultString = str + val;


* This source code was highlighted with Source Code Highlighter .


In the last line we see the operation of adding two variables of different types, the result of which will be a string. Since the variables are of different types, the next version of the string System method will be called. String .Concat ( object , object ). Those. the actual program code will be as follows

string str = "habrahabr" ;

int val = 0;

var resultString = System. String .Concat(str, val);


* This source code was highlighted with Source Code Highlighter .


When passing the str (string - reference type) and val (integer - significant type) parameters to the Concat method for the second parameter, a packing operation will be performed, because we are trying to pass a significant int type to the method, and he expects a reference object.



What is packaging?



For exact definition, I turned to Richter ’s book “CLR via c #” . So:

Packing (boxing) is the conversion of a meaningful type into a reference. When packing an instance of a significant type, the following occurs.

1. The managed heap is allocated memory. Its volume is determined by the length of the significant type and the two additional members required for all objects in the managed heap — a pointer to the type object and the SyncBlockIndex index.

2. Fields of significant type are copied to the memory just allocated on the heap.

3. Returns the address of the object. This address is an object reference; the significant type has become reference.


And if you still call ToString ()



If you call the ToString () method on the val variable before passing it to the System.String.Concat () method, the compiler will choose the next version of the overloaded string System string method. String .Concat ( string , string ), because Objects of the same reference type string will be already input. In this case, the packing operation will not be performed, which implies allocating memory for the significant type on the heap, copying there all the bytes of the original variable of the significant type, and returning the pointer to the allocated memory.



What is it compiled to?



Without calling ToString ()

  1. .locals init ([0] string str, [1] int32 val, [2] string resultString)
  2. IL_0000: nop
  3. IL_0001: ldstr "habrahabr"
  4. IL_0006: stloc.0
  5. IL_0007: ldc.i4.0
  6. IL_0008: stloc.1
  7. IL_0009: ldloc.0
  8. IL_000a: ldloc.1
  9. IL_000b: box [mscorlib] System. Int32
  10. IL_0010: call string [mscorlib] System. String :: Concat ( object , object )
  11. IL_0015: stloc.2
* This source code was highlighted with Source Code Highlighter .


With a call tostring ()

  1. .locals init ([0] string str, [1] int32 val, [2] string resultString)
  2. IL_0000: nop
  3. IL_0001: ldstr "habrahabr"
  4. IL_0006: stloc.0
  5. IL_0007: ldc.i4.0
  6. IL_0008: stloc.1
  7. IL_0009: ldloc.0
  8. IL_000a: ldloca.s val
  9. IL_000c: call instance string [mscorlib] System. Int32 :: ToString ()
  10. IL_0011: call string [mscorlib] System. String :: Concat ( string , string )
  11. IL_0016: stloc.2
* This source code was highlighted with Source Code Highlighter .


The main difference in the given IL codes is the box instruction that performs the packing operation, the opposite unpacking command corresponds to the unbox instruction, I do not consider it here.

')

Performance impact



Obviously, it is difficult to judge the performance of the above code, since a single packing operation occurs fairly quickly. We write the next cycle

for ( int i = 0; i < 10000000; i++)

{

var s = "habrahabr" + i;

}


* This source code was highlighted with Source Code Highlighter .


To measure runtime, we use the Stopwatch class from the System.Diagnostics namespace, since It gives a much more accurate result than using DateTime. For example, on my machine, calling two consecutive DateTime.Now gives the difference 00: 00: 00.0010000, and starting and immediately stopping Stopwatch - 00: 00: 00.0000015. The difference is the Windows with the naked eye.

The final code with which we will test the packing operation will be as follows.

namespace BoxingTraining

{

using System;

using System.Diagnostics;

public class Program

{

private static void Main()

{

var time = Stopwatch.StartNew();

for ( int i = 0; i < 10000000; i++)

{

var s = "habrahabr" + i;

}

Console .WriteLine(time.Elapsed.ToString());

}

}

}



* This source code was highlighted with Source Code Highlighter .


Below is a table with the results of running the above code when packing the value of int (4 bytes)

The number of iterations in the loopTime, s.Difference, c.Winning,%
Without ToString ()With ToString ()
100,0000.03244210.03148380,00095833.043788
1,000,0000.33298100,29270900.040272013.75837
10,000,0003.53443303.24258430.29184879,000497
100,000,00035.902293735,42089820.48139551.359072


As you can see, the results, though not in favor of packaging, are not as terrible as they might have seemed at once. Further, we can assume that the size of the type (the value of the occupied memory) can also affect the result, and the next step will be the packaging of a single value of type char, byte and some kind of heavy, self-written structure.

The code for testing the packaging of any variables of significant type.

namespace boxingTraining

{

using System;

using System.Diagnostics;

public class Program

{

private static void Main()

{

var time = Stopwatch.StartNew();

for ( int i = 0; i < 1000000; i++)

{

var s = "habrahabr" + ;

}

Console .WriteLine(time.Elapsed.ToString());

}

}

}


* This source code was highlighted with Source Code Highlighter .


The result table for char (2 bytes)

The number of iterations in the loopTime, s.Difference, c.Winning,%
Without ToString ()With ToString ()
100,0000,01201200,00802800,00398449.62631
1,000,0000.09255450.07386900.018685525,29546
10,000,0000.89496940.72985980.165109622.6221
100,000,0009,19085566,99771692,193138731,34077


Result table for byte (1 byte)

The number of iterations in the loopTime, s.Difference, c.Winning,%
Without ToString ()With ToString ()
100,0000.02643630.02422110,00221529,145745
1,000,0000.26006720,23041880.029648412,86718
10,000,0002.55634602,27130210.285043912,5498
100,000,00025,184794422,30633522,878459212,90422


A small lyrical digression. When I wrote this topic and came to this point, habrauzer Aldanko , with whom I consulted when writing this article, strongly recommended testing for more standard types to see how they behave in this situation and to investigate the effect of the internal device on packing operation.

Let's test for the following types System.Int16 (2 bytes), System.Int64 (8 bytes), System.Single (4 bytes), System.Double (8 bytes) and System.Decimal (16 bytes).

For System.Int16

The number of iterations in the loopTime, s.Difference, c.Winning,%
Without ToString ()With ToString ()
100,0000.02682690.02527530,0015526.1388
1,000,0000,25081710,22831340.0225049,856496
10,000,0002,58401732,27711030,30690713,47792
100,000,00025.557501422,63220242,92529912,92538


For System.Int64

The number of iterations in the loopTime, s.Difference, c.Winning,%
Without ToString ()With ToString ()
100,0000.03132520.02615760,00516819,75564
1,000,0000.27304050.25202120.0210198,34029
10,000,0002.75734222.30897440.44836819,41848
100,000,00026,687696423.35651233.33118414,26234


For System.Single

The number of iterations in the loopTime, s.Difference, c.Winning,%
Without ToString ()With ToString ()
100,0000.04882710.04351740,0053112,20133
1,000,0000.45858080.42773030.0308517.212606
10,000,0004,50099574,23132420.2696716.373218
100,000,00044,990615442,57028072,4203355,685503


For System.Double

The number of iterations in the loopTime, s.Difference, c.Winning,%
Without ToString ()With ToString ()
100,0000.04772930.04540060,0023295,129227
1,000,0000.47749110.45076110.026735.92997
10,000,0004,74261564.52359230.2190234.8418
100,000,00047.481688144.33830823.143387.089535


For System.Decimal

The number of iterations in the loopTime, s.Difference, c.Winning,%
Without ToString ()With ToString ()
100,0000.03422770.03023810,0039913,19395
1,000,0000.30824510.28527630.0229698.051422
10,000,0003.05176152,81427090.2374918,438797
100,000,00030.782424127.81044802.97197610.68655


As you can see, the call toString () is always faster. In principle, it cannot be slow, because if it is not called, it will be called anyway, but before that, an object of a significant type will be wrapped.

Performance of user structures



I have already tested the most common standard types. It is time to write your structure. On the advice of the same habrayuzer, Aldanko will write a structure containing 1600 fields of type byte (1 byte) and 100 fields of type Decimal (16 byte). The first structure is as follows.

public struct DecimalStruct

{

public decimal Field1;

public decimal Field2;

public decimal Field3;

public decimal Field4;

...

public decimal Field97;

public decimal Field98;

public decimal Field99;

public decimal Field100;

public override string ToString()

{

return "DecimalStruct" ;

}

}


* This source code was highlighted with Source Code Highlighter .


You can download the full version of the code.

Structure with 1600 byte type fields

public struct ByteStruct

{

public byte Field1;

public byte Field2;

public byte Field3;

public byte Field4;

...

public byte Field1597;

public byte Field1598;

public byte Field1599;

public byte Field1600;

public override string ToString()

{

return "ByteStruct" ;

}

}


* This source code was highlighted with Source Code Highlighter .


You can download the full version of the code.

Correspondingly, the code for which we will test the packing operation will be as follows

namespace boxingTraining

{

using System;

using System.Diagnostics;

public class Program

{

private static void Main()

{

var myStruct = new ByteStruct(); // new DecimalStruct()

var time = Stopwatch.StartNew();



for ( int i = 0; i < 100000; i++)

{

var s = "habrahabr" + myStruct; // myStruct.ToString()

}

Console .WriteLine(time.Elapsed.ToString());

}

}

}


* This source code was highlighted with Source Code Highlighter .


The purpose of these frauds is to see how the internal structure of the structure will affect the time of the packing operation, namely, to copy the internal data into a managed heap.

So the results. For ByteStruct

The number of iterations in the loopTime, s.Difference, c.Winning,%
Without ToString ()With ToString ()
100,0000.05538110,00727600.048105661.1476
1,000,0000.52180770.05911200.462696782,7441
10,000,0005,20838780.55200374.656384843,5422
100,000,00061,81427505,544944856,269331014,786


Total for DecimalString

The number of iterations in the loopTime, s.Difference, c.Winning,%
Without ToString ()With ToString ()
100,0000.08156640,00685410.0747121090.038
1,000,0000.82086480.06388180.7569831184,974
10,000,0006.93492830.62493096,3099971009,711
100,000,00054,71892056,845334947.87359699,3608


As can be seen from the above tests on voluminous structures, the ToString call gives a gain up to 10 times. True so that it is clearly visible, you need to repeat the packing operation more than once.



findings



What can we conclude from this mini-study? At least it is obvious to me. For reference types, a call to ToString () will not give any benefit and its use in string operations will be redundant, but for meaningful types, a call to ToString () is necessary. When you call it, we avoid a fairly resource-intensive operation - packaging, which in certain cases can significantly degrade the performance of the code. Well, R # turned out to be wrong, cursing that, the call ToString () of a meaningful type is redundant when concatenating strings. It can be not produced, but you can pay the performance.



Progg it

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



All Articles