Подтвердить что ты не робот

Stackoverflow делает бокс в С#

У меня эти два куска кода в С#:

Первая

class Program
{
    static Stack<int> S = new Stack<int>();

    static int Foo(int n) {
        if (n == 0)
            return 0;
        S.Push(0);
        S.Push(1);
        ...
        S.Push(999);
        return Foo( n-1 );
    }
}

Второй

class Program
{
    static Stack S = new Stack();

    static int Foo(int n) {
        if (n == 0)
            return 0;
        S.Push(0);
        S.Push(1);
        ...
        S.Push(999);
        return Foo( n-1 );
    }
}

Оба они делают то же самое:

  • Создайте стек (общий для <int> для первого примера и стек объекта для второго).

  • Объявите метод, который вызывает себя рекурсивно n раз (n >= 0), и на каждом шаге нажимайте 1000 целых чисел внутри созданного стека.

Когда я запускаю первый пример с Foo(30000), исключение не возникает, но второй пример сбрасывается с помощью Foo(1000), только n = 1000.

Когда я увидел CIL, сгенерированный для обоих случаев, единственной разницей была бокс-часть для каждого нажатия:

Первая

IL_0030:  ldsfld     class [System]System.Collections.Generic.Stack`1<int32> Test.Program::S
IL_0035:  ldc.i4     0x3e7
IL_003a:  callvirt   instance void class [System]System.Collections.Generic.Stack`1<int32>::Push(!0)
IL_003f:  nop

Второй

IL_003a:  ldsfld     class [mscorlib]System.Collections.Stack Test.Program::S
IL_003f:  ldc.i4     0x3e7
IL_0044:  box        [mscorlib]System.Int32
IL_0049:  callvirt   instance void [mscorlib]System.Collections.Stack::Push(object)
IL_004e:  nop

Мой вопрос: почему, если нет второстепенной перегрузки стека CIL для второго примера, происходит ли он "быстрее", чем первый?

4b9b3361

Ответ 1

Почему, если нет существенной перегрузки стека CIL для второго примера, происходит ли он "быстрее", чем первый?

Обратите внимание, что количество инструкций CIL не точно отражает объем работы или памяти, которые будут использоваться. Единая инструкция может быть очень низкой или очень высокой, поэтому подсчет инструкций CIL не является точным способом измерения "работы".

Также поймите, что CIL - это не то, что выполняется. JIT компилирует CIL в действительные машинные инструкции с фазой оптимизации, поэтому CIL может сильно отличаться от фактических выполненных инструкций.

Во втором случае, поскольку вы используете не-общую коллекцию, каждый вызов Push требует, чтобы целое число было помещено в бокс, как вы определили в CIL.

Бокс целое эффективно создает объект, который "обертывает" Int32 для вас. Вместо того, чтобы просто загружать 32-битное целое в стек, теперь ему нужно загрузить 32-разрядное целое в стек, а затем вставить его, что также эффективно загружает ссылку на объект в стек.

Если вы проверите это в окне "Разборка", вы увидите, что разница между общей и не-общей версией является драматической и гораздо более значительной, чем предлагаемый CIL.

Общая версия эффективно компилируется как серия вызовов вроде:

0000022c  nop 
            S.Push(25);
0000022d  mov         ecx,dword ptr ds:[03834978h] 
00000233  mov         edx,19h 
00000238  cmp         dword ptr [ecx],ecx 
0000023a  call        71618DD0 
0000023f  nop 
            S.Push(26);
00000240  mov         ecx,dword ptr ds:[03834978h] 
00000246  mov         edx,1Ah 
0000024b  cmp         dword ptr [ecx],ecx 
0000024d  call        71618DD0 
00000252  nop 
            S.Push(27);

Ненулезный, с другой стороны, должен создавать объекты в штучной упаковке и вместо этого компилируется в:

00000645  nop 
            S.Push(25);
00000646  mov         ecx,7326560Ch 
0000064b  call        FAAC20B0 
00000650  mov         dword ptr [ebp-48h],eax 
00000653  mov         eax,dword ptr ds:[03AF4978h] 
00000658  mov         dword ptr [ebp+FFFFFEE8h],eax 
0000065e  mov         eax,dword ptr [ebp-48h] 
00000661  mov         dword ptr [eax+4],19h 
00000668  mov         eax,dword ptr [ebp-48h] 
0000066b  mov         dword ptr [ebp+FFFFFEE4h],eax 
00000671  mov         ecx,dword ptr [ebp+FFFFFEE8h] 
00000677  mov         edx,dword ptr [ebp+FFFFFEE4h] 
0000067d  mov         eax,dword ptr [ecx] 
0000067f  mov         eax,dword ptr [eax+2Ch] 
00000682  call        dword ptr [eax+18h] 
00000685  nop 
            S.Push(26);
00000686  mov         ecx,7326560Ch 
0000068b  call        FAAC20B0 
00000690  mov         dword ptr [ebp-48h],eax 
00000693  mov         eax,dword ptr ds:[03AF4978h] 
00000698  mov         dword ptr [ebp+FFFFFEE0h],eax 
0000069e  mov         eax,dword ptr [ebp-48h] 
000006a1  mov         dword ptr [eax+4],1Ah 
000006a8  mov         eax,dword ptr [ebp-48h] 
000006ab  mov         dword ptr [ebp+FFFFFEDCh],eax 
000006b1  mov         ecx,dword ptr [ebp+FFFFFEE0h] 
000006b7  mov         edx,dword ptr [ebp+FFFFFEDCh] 
000006bd  mov         eax,dword ptr [ecx] 
000006bf  mov         eax,dword ptr [eax+2Ch] 
000006c2  call        dword ptr [eax+18h] 
000006c5  nop 

Здесь вы можете увидеть значение бокса.

В вашем случае при боксе целое число вызывает привязку ссылок на бокс-объекты в стек. В моей системе это вызывает stackoverflow при любых вызовах, превышающих Foo(127) (в 32 бит), что говорит о том, что целые числа и ссылки на объекты в боксе (по 4 байта) все хранятся в стеке, так как 127 * 1000 * 8 == 1016000, что опасно близко к размеру стека по размеру потока 1 по умолчанию для приложений .NET.

При использовании универсальной версии, поскольку нет объекта в штучной упаковке, целые числа не должны быть сохранены в стеке, и один и тот же регистр повторно используется. Это позволяет вам значительно увеличить ( > 40000 в моей системе), прежде чем использовать стек.

Обратите внимание, что это будет версия CLR и зависимая от платформы, так как есть и другая JIT на x86/x64.