Late-bound Array SetValue
The type "System.Array" provides us a set of API for the late-bound array operations, including array creation (Array.CreateInstance), read/write access to array element (Array.SetValue/Array.GetValue). They are convenient to use. Let me start with some code to create an one-dimensional integer array, and set the first 2 elements using early-bind assignment and Array.SetValue, respectively. To simplify my code, this post will only touch the late-bound set value on array (the similar discussion can be applied to get value).
// creation: new int[5]
Array x = Array.CreateInstance(typeof(int), 5);
// 0. early-bind set: explicit cast to int[]
int[] y = (int[])x;
y[0] = 100;
// 1. late-bound set via Array.SetValue
x.SetValue(200, 1);
From the signature of Array.SetValue(object, int), we know that the integer "200" has to be boxed first before passing into this API, which is a performance hit (among others). However, we can take a look at what kind of IL sequence C# compiler generates for the early-bound assignment, and build an equivalent dynamic method to avoid the boxing: C# compiler emits Stelem.I4 to store the 32-bit integer value.
// 2. late-bound set via DynamicMethod/Delegate/OpCodes.Stelem
DynamicMethod dm = new DynamicMethod("SetValueByStelem", typeof(void),
new Type[] { typeof(Array), typeof(int), typeof(int) }, typeof(Test));
ILGenerator ilgen = dm.GetILGenerator();
ilgen.Emit(OpCodes.Ldarg_0);
ilgen.Emit(OpCodes.Castclass, x.GetType());
ilgen.Emit(OpCodes.Ldarg_1); // index
ilgen.Emit(OpCodes.Ldarg_2); // value
ilgen.Emit(OpCodes.Stelem_I4);
ilgen.Emit(OpCodes.Ret);
MySetValue sv = (MySetValue)dm.CreateDelegate(typeof(MySetValue));
sv(x, 2, 300);
delegate void MySetValue(Array array, int index, int value);
The runtime (so is System.Array) supports non-zero lowerbound array (C# does not). The code snippet below creates a length-of-5 integer array with -3 as its' lower-bound, and then tries to set value for some elements.
- We can no longer cast such array to int[].
- Array.SetValue works. Note we need use the lowerbound value to access the first element.
- The instructions "stelem/stelem.<type>" are for one dimensional array with zero-based index only. So the dynamic method approach with stelem instruction won't work.
- For each concrete array type, runtime adds three special methods: Get/Set/Address. With reflection we can hold those MethodInfo, so you can call MethodInfo.Invoke or create another dynamic method wrapper around it. This works for zero-based array too.
// creation: new int[-3..1]
Array x = Array.CreateInstance(typeof(int), new int[] { 5 }, new int[] { -3 });
// 1. late-bound set via Array.SetValue
x.SetValue(200, 1 - 3);
// 3. late-bound set via MethodInfo.Invoke of "Set"
MethodInfo mi = x.GetType().GetMethod("Set");
mi.Invoke(x, new object[] { 3 - 3, 400 });
// 4. late-bound set via DynamicMethod/Delegate/"Set"
DynamicMethod dm = new DynamicMethod("SetValueBySet", typeof(void),
new Type[] { typeof(Array), typeof(int), typeof(int) }, typeof(Test));
ILGenerator ilgen = dm.GetILGenerator();
ilgen.Emit(OpCodes.Ldarg_0);
ilgen.Emit(OpCodes.Castclass, x.GetType());
ilgen.Emit(OpCodes.Ldarg_1);
ilgen.Emit(OpCodes.Ldarg_2);
ilgen.Emit(OpCodes.Call, mi);
ilgen.Emit(OpCodes.Ret);
MySetValue sv = (MySetValue)dm.CreateDelegate(typeof(MySetValue));
sv(x, 4 - 3, 500);
Using Vance's CodeTimers class, I measured these 5 approaches' performance on my Vista machine. My scenario is to set value on 1-dimensional zero-based integer array, the action loop contains 10 lines of set-value code (those dynamic method and delegate creation are prepared ahead). The fastest late-bound set approach is DynamicMethod with Stelem, about 90-times slower than the early-bound assignment; but it is 3-times faster than Array.SetValue. Here is the raw output:
Data units of msec resolution = 0.279365 usec
Early-bound assignment : count: 1000000 4.525 +- 1% msec
Late-bound set via Array.SetValue : count: 1000000 1138.182 +- 2% msec
Late-bound set via DynamicMethod/Stelem : count: 1000000 397.005 +- 0% msec
Late-bound set via Set MethodInfo.Invoke: count: 1000000 36352.360 +- 0% msec
Late-bound set via DynamicMethod/Set : count: 1000000 453.956 +- 7% msec
What about multi-dimensional array? Array.SetValue still works; for better performance, we might want use the special "Set" method. By the way, the following ildasm output shows what C# compiler generates for "array[0, 1] = 1000;", which uses "Set". Note "int32[0..., 0...]" is TypeSpec(0x1B).
IL_0009: ldloc.0
IL_000a: ldc.i4.0
IL_000b: ldc.i4.1
IL_000c: ldc.i4 0x3e8
IL_0011: call instance void int32[0...,0...]/* 1B000003 */::Set(int32, int32, int32) /* 0A000020 */