Cascade Assignment

Cascade assignment is the concept of having the same value assigned to different variables in one line. For example:

  1. copy1 = copy2 = copy3 = some_value;

This article examines whether cascade assignment differentiate from having different assignment statements for each assignment operator.

For the test scenarios consider the constructor of square shape where all sides are initialised to the same value.

Test Scenario 1:

Let’s start by a simple scenario where all the variables are of the same type.

  1. int side1 = 10;
  2. int side2 = side1;
  3. int side3 = side1;
  4. int side4 = side1;

The MSIL code that is generated for the above 4 assignment statements is

  1. ldc.i4.s   10
  2. stloc.0
  3. ldloc.0
  4. stloc.1
  5. ldloc.0
  6. stloc.2
  7. ldloc.0
  8. stloc.3

Test Scenario 2:

Now modifying the code to use cascade assignment

  1. int side1 = 10;
  2. int side2, side3, side4;
  3.  
  4. side2 = side3 = side4 = side1;

The new MSIL code that is generated by the compiler is

  1. ldc.i4.s   10
  2. stloc.0
  3. ldloc.0
  4. dup
  5. stloc.3
  6. dup
  7. stloc.2
  8. stloc.1

When comparing the two MSIL codes the first 3 lines of the code generated are exactly the same. This is expected as for the first two assignment operations nothing has changed.

  1. int side1 = 10;
  2. side4 = side1;

However when comparing the remaining code it is immediately noticeable that wherever in the first code we had the loading of initial variable in memory now there is the op code dup. The dup instruction instructs the .NET run-time to reuse the current top-most value on the stack.

Test Scenario 3:

When using a primitive data type there seems to be only a minimal difference between having a different assignment statement or using cascade assignment.

Let’s consider that this time rather than working with the same data type we need to convert the values to their string representation. In articles String concatenation and int indirect cast and Using ToString() on the same item multiple times the benefits of using the ToString() and storing its value into a variable that is referenced in different parts of the code has been examined. Now let’s see if using the cascade assignment can remove the necessity of creating a variable to store the ToString() result when performing consecutive assignments of the same value representation.

  1. int side = 10;
  2.  
  3. string side1 = side.ToString();
  4. string side2 = side.ToString();
  5. string side3 = side.ToString();
  6. string side4 = side.ToString();

The code that is generated to handle the above C# code is

  1. ldc.i4.s   10
  2. stloc.0
  3. ldloca.s   side
  4. call       instance string [mscorlib]System.Int32::ToString()
  5. stloc.1
  6. ldloca.s   side
  7. call       instance string [mscorlib]System.Int32::ToString()
  8. stloc.2
  9. ldloca.s   side
  10. call       instance string [mscorlib]System.Int32::ToString()
  11. stloc.3
  12. ldloca.s   side
  13. call       instance string [mscorlib]System.Int32::ToString()
  14. stloc.s    side4

This confirms the outcomes from the previous two articles that the compiler will not optimize the statements to have only one call to the ToString() method.

Modifying the code to have only one call to the ToString()

  1. int side = 10;
  2.  
  3. string side1 = side.ToString();
  4. string side2 = side1;
  5. string side3 = side1;
  6. string side4 = side1;

Results in the generation of following instructions

  1. ldc.i4.s   10
  2. stloc.0
  3. ldloca.s   side
  4. call       instance string [mscorlib]System.Int32::ToString()
  5. stloc.1
  6. ldloc.1
  7. stloc.2
  8. ldloc.1
  9. stloc.3
  10. ldloc.1

Test Scenario 4:

Now it is time to check whether the cascade assignment will produce the same optimised code as the last example.

  1. int side = 10;
  2. string side1, side2, side3, side4;
  3.  
  4. side1 = side2 = side3 = side4 = side.ToString();

Where the code produced by the compiler ends up to

  1. ldc.i4.s   10
  2. stloc.0
  3. ldloca.s   side
  4. call       instance string [mscorlib]System.Int32::ToString()
  5. dup
  6. stloc.s    side4
  7. dup
  8. stloc.3
  9. dup
  10. stloc.2
  11. stloc.1

Test Scenario 5:

The cascade assignment seems to be suitable where data conversion is required as it avoids the need of creating a temporary variable.

To determine whether the cascade assignment can actually provide this benefit, let’s modify the program to perform a numeric data type conversion.

  1. int side1 = 10;
  2. double side2, side3, side4;
  3.  
  4. side2 = (double) side1;
  5. side3 = (double) side1;
  6. side4 = (double) side1;

The code generated by the multiple assignment statements is

  1. ldc.i4.s   10
  2. stloc.0
  3. ldloc.0
  4. conv.r8
  5. stloc.1
  6. ldloc.0
  7. conv.r8
  8. stloc.2
  9. ldloc.0
  10. conv.r8
  11. stloc.3

The MSIL code that is generated is shows that every time the type casting has been invoked in the code it resulted in the two operations. The first operation is to load the value stored in the variable into memory and the second operator to convert the value. Will the cascade assignment concept help to reduce the number of operations required?

To answer this question let’s modify the code once again to use the cascade assignment technique

  1. int side = 10;
  2. double side1, side2, side3, side4;
  3.  
  4. side1 = side2 = side3 = side4 = (double) side;

The new code results in the following MSIL code:

  1. ldc.i4.s   10
  2. stloc.0
  3. ldloc.0
  4. conv.r8
  5. dup
  6. stloc.s    side4
  7. dup
  8. stloc.3
  9. dup
  10. stloc.2
  11. stloc.1

It can be concluded that the cascade assignment provide some minor performance gains when type casting is involved by reducing the number of operations required.

References: