Right now, I'm just going to look at the AddAndMultiply function, and see how lazy evaluation was handled. Here is the CIL code:
.maxstack 11
IL_0000: ldarg.1
IL_0001: callvirt instance !0 class ToylModel20130322.ISignature`1<int32>::Value()
IL_0006: ldc.i4.0
IL_0007: bge IL_0017
IL_000c: ldstr "x must not be less than zero"
IL_0011: newobj instance void class [mscorlib]System.Exception::'.ctor'(string)
IL_0016: throw
IL_0017: ldarg.2
IL_0018: callvirt instance !0 class ToylModel20130322.ISignature`1<int32>::Value()
IL_001d: ldarg.1
IL_001e: callvirt instance !0 class ToylModel20130322.ISignature`1<int32>::Value()
IL_0023: add
IL_0024: ldarg.1
IL_0025: callvirt instance !0 class ToylModel20130322.ISignature`1<int32>::Value()
IL_002a: mul
IL_002b: ret
Fig. 11.1: CIL code for AddAndMultiply
Let's just walk through this.
ldarg.1
This loads the function's first argument (ISignature<int> x) onto the stack.
Stack now has: [x]
callvirt instance !0 class ToylModel20130322.ISignature`1<int32>::Value()
This calls the Value() method of the ISignature<int32> interface. This will pop the x off of the stack and push the result of the evaluation back on the stack. This is how we implemented lazy evaluation.
Stack now has: [x.Value]
ldc.i4.0
This pushes the constant value 0 onto the stack.
Stack now has: [x.Value, 0]
bge IL_0017
This takes the last two values from the stack. If the next-to-top value is greater than or equal to the top value (i.e. if 13 >= 0) then it will branch to the operation at IL_0017. This pops both values off the stack.
Stack now has: []
The next few steps would be followed if we had passed in an x that evaluated to a negative value.
ldstr "x must not be less than zero"
This loads the exception message string onto the stack.
Stack now has: ["x must not be less than zero"]
newobj instance void class [mscorlib]System.Exception::'.ctor'(string)
This creates a new instance of the Exception class. The constructor takes one parameter, so it pops the string off the stack. The new exception is pushed back onto the stack.
Stack now has: [Exception]
throw
This pops the exception off the stack and throws it. Execution ends here.
The following steps would be executed for non-negative values of x.
ldarg.2
callvirt instance !0 class ToylModel20130322.ISignature`1<int32>::Value()
This loads the function's second argument (ISignature<int> y) onto the stack and calls the Value() method.
Stack now has: [y.Value]
ldarg.1
callvirt instance !0 class ToylModel20130322.ISignature`1<int32>::Value()
This again loads the function's first argument (ISignature<int> x) onto the stack and calls the Value() method.
Stack now has: [y.Value, x.Value]
add
This adds the top two values on the stack and pushes the result back on.
Stack now has: [(y.Value + x.Value)]
ldarg.1
callvirt instance !0 class ToylModel20130322.ISignature`1<int32>::Value()
For the third time, this loads the function's first argument (ISignature<int> x) onto the stack and calls the Value() method.
Stack now has: [(y.Value + x.Value), x.Value]
mul
This multiplies the top two values on the stack and pushes the result back on.
Stack now has: [(y.Value + x.Value) * x.Value]
ret
This returns the value on the top of the stack.
The main lesson I learned from looking at this is that I ended up calling the Value() method four times, when I only really needed it twice. It probably would have been better if the C# code looked like this:
int _x = x.Value ();
if (_x < 0) {
throw new Exception("x must not be less than zero");
} else {
int _y = y.Value();
return (_y + _x) * _x;
}
Fig. 11.2: More efficient C# code
This code only calls Value() once and stores the result locally.
No comments:
Post a Comment