Ответ 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.