Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Testing foreach box/unbox on a regular array?

I've read this excellent question about how a regular int[] is handled under foreach ( box or not) loop.

Array does implement the non-generic IEnumerable so it has to use anobject inside ( and not int)

But it turns out that - in run-time it actually handled as IEnumerable<T>

How can I test/proof (that there isn't boxing )it by a simple C# code? ( and not by reading IL.)

like image 414
Royi Namir Avatar asked Oct 18 '25 15:10

Royi Namir


2 Answers

I like @phoog's answer, so just for fun :)

Helper class

public static class ILUtils
{
    private static Dictionary<short, OpCode> s_opcodes = new Dictionary<short, OpCode>();

    static ILUtils()
    {
        FieldInfo[] opCodeFields = typeof(OpCodes).GetFields(BindingFlags.Public | BindingFlags.Static);
        foreach (FieldInfo opCodeField in opCodeFields)
        {
            if (opCodeField.FieldType != typeof(OpCode))
                continue;

            OpCode opcode = (OpCode)opCodeField.GetValue(null);
            s_opcodes.Add(opcode.Value, opcode);
        }
    }

    public static bool ContainsOpcodes(MethodInfo methodInfo, IEnumerable<OpCode> targetOpCodes)
    {
        MethodBody methodBody = methodInfo.GetMethodBody();

        using (BinaryReader ilReader = new BinaryReader(new MemoryStream(methodBody.GetILAsByteArray())))
        {
            while (ilReader.BaseStream.Position < ilReader.BaseStream.Length)
            {
                short opCodeValue = ilReader.ReadByte();
                if (opCodeValue == 0xfe)
                    opCodeValue = (short)(opCodeValue << 8 | ilReader.ReadByte());

                OpCode opCode = s_opcodes[opCodeValue];
                if (targetOpCodes.Contains(opCode))
                    return true;

                int argumentSize = 4;
                if (opCode.OperandType == OperandType.InlineNone)
                    argumentSize = 0;
                else if (opCode.OperandType == OperandType.ShortInlineBrTarget || opCode.OperandType == OperandType.ShortInlineI || opCode.OperandType == OperandType.ShortInlineVar)
                    argumentSize = 1;
                else if (opCode.OperandType == OperandType.InlineVar)
                    argumentSize = 2;
                else if (opCode.OperandType == OperandType.InlineI8 || opCode.OperandType == OperandType.InlineR)
                    argumentSize = 8;
                else if (opCode.OperandType == OperandType.InlineSwitch)
                {
                    int num = ilReader.ReadInt32();
                    argumentSize = (int)(4 * num + 4);
                }

                ilReader.BaseStream.Position += argumentSize;
            }
        }

        return false;
    }
}

Example usage

private static void BoxingForEach()
{
    IEnumerable foo = (IEnumerable)new int[10];
    foreach (int i in foo) ;
}

private static void NoBoxingForEach()
{
    int[] foo = new int[10];
    foreach (int i in foo) ;
}

static void Main(string[] args)
{
    MethodInfo boxingForEach = typeof(Program).GetMethod("BoxingForEach", BindingFlags.Static | BindingFlags.NonPublic);
    MethodInfo noBoxingForEach = typeof(Program).GetMethod("NoBoxingForEach", BindingFlags.Static | BindingFlags.NonPublic);

    Console.WriteLine("BoxingForEach is using boxing: {0}", 
        ILUtils.ContainsOpcodes(boxingForEach, new[] { OpCodes.Box, OpCodes.Unbox, OpCodes.Unbox_Any }));

    Console.WriteLine("NoBoxingForEach is using boxing: {0}", 
        ILUtils.ContainsOpcodes(noBoxingForEach, new[] { OpCodes.Box, OpCodes.Unbox, OpCodes.Unbox_Any }));
}

Results

BoxingForEach is using boxing: True

NoBoxingForEach is using boxing: False

like image 67
Nikolay Khil Avatar answered Oct 21 '25 03:10

Nikolay Khil


Your question assumes incorrectly (as does the question you link to) that arrays do not implement the generic IEnumerable<T>. They do. You can see this using reflection:

var array = new int[0];
var enumerator = array.GetEnumerator();
var enumeratorType = enumerator.GetType();
var propertyInfo = enumeratorType.GetProperty("Current");
var propertyType = propertyInfo.PropertyType;
Console.WriteLine(propertyType.Name); //prints "Object";
var otherEnumerator = ((IEnumerable<int>)array).GetEnumerator();
enumeratorType = otherEnumerator.GetType();
propertyInfo = enumeratorType.GetProperty("Current");
propertyType = propertyInfo.PropertyType;
Console.WriteLine(propertyType.Name); //prints "Int32";

However, if you write a foreach loop on a statically-typed array reference, the C# compiler translates it to a for loop. I don't think there's any way to check that without looking at the IL.

From http://msdn.microsoft.com/en-us/library/system.array.aspx:

Important

Starting with the .NET Framework 2.0, the Array class implements the System.Collections.Generic.IList<T>, System.Collections.Generic.ICollection<T>, and System.Collections.Generic.IEnumerable<T> generic interfaces. The implementations are provided to arrays at run time, and therefore are not visible to the documentation build tools. As a result, the generic interfaces do not appear in the declaration syntax for the Array class, and there are no reference topics for interface members that are accessible only by casting an array to the generic interface type (explicit interface implementations). The key thing to be aware of when you cast an array to one of these interfaces is that members which add, insert, or remove elements throw NotSupportedException.

like image 36
phoog Avatar answered Oct 21 '25 04:10

phoog