Ответ 1
Вы используете оба примера в одной и той же архитектуре. Я получаю ~ 1.4 сек на x64 для кода F # и С# и ~ 0,6 сек на x86 для F # и ~ 0.3 сек на x86 для С#.
Как вы говорите, при декомпиляции сборок код выглядит довольно похожим, но при анализе кода IL появляются некоторые диссимиляции:
F # - let min (struct(a1, b1)) (struct(a2, b2)) ...
.maxstack 5
.locals init (
[0] int32 b1,
[1] int32 a1,
[2] int32 b2,
[3] int32 a2
)
IL_0000: ldarga.s _arg2
IL_0002: ldfld !1 valuetype [System.ValueTuple]System.ValueTuple`2<int32, int32>::Item2
IL_0007: stloc.0
IL_0008: ldarga.s _arg2
IL_000a: ldfld !0 valuetype [System.ValueTuple]System.ValueTuple`2<int32, int32>::Item1
IL_000f: stloc.1
IL_0010: ldarga.s _arg1
IL_0012: ldfld !1 valuetype [System.ValueTuple]System.ValueTuple`2<int32, int32>::Item2
IL_0017: stloc.2
IL_0018: ldarga.s _arg1
IL_001a: ldfld !0 valuetype [System.ValueTuple]System.ValueTuple`2<int32, int32>::Item1
IL_001f: stloc.3
IL_0020: nop
IL_0021: ldloc.1
IL_0022: ldloc.3
IL_0023: call int32 Program::[email protected](int32, int32)
IL_0028: ldloc.0
IL_0029: ldloc.2
IL_002a: call int32 Program::[email protected](int32, int32)
IL_002f: newobj instance void valuetype [System.ValueTuple]System.ValueTuple`2<int32, int32>::.ctor(!0, !1)
IL_0034: ret
С# - MinPair
.maxstack 3
.locals init (
[0] int32 b,
[1] int32 b2,
[2] int32 a2
)
IL_0000: ldarg.0
IL_0001: ldfld !1 valuetype [System.ValueTuple]System.ValueTuple`2<int32, int32>::Item2
IL_0006: stloc.0
IL_0007: ldarg.0
IL_0008: ldfld !0 valuetype [System.ValueTuple]System.ValueTuple`2<int32, int32>::Item1
IL_000d: ldarg.1
IL_000e: ldfld !1 valuetype [System.ValueTuple]System.ValueTuple`2<int32, int32>::Item2
IL_0013: stloc.1
IL_0014: ldarg.1
IL_0015: ldfld !0 valuetype [System.ValueTuple]System.ValueTuple`2<int32, int32>::Item1
IL_001a: stloc.2
IL_001b: ldloc.2
IL_001c: call int32 PerfItCs.Program::MinInt(int32, int32)
IL_0021: ldloc.0
IL_0022: ldloc.1
IL_0023: call int32 PerfItCs.Program::MinInt(int32, int32)
IL_0028: newobj instance void valuetype [System.ValueTuple]System.ValueTuple`2<int32, int32>::.ctor(!0, !1)
IL_002d: ret
Разница здесь в том, что компилятор С# избегает введения некоторых локальных переменных путем нажатия промежуточных результатов в стеке. Поскольку локальные переменные выделяются в стеке, все равно трудно понять, почему это должно привести к более эффективному коду.
Другие функции очень похожи.
Разборка x86 дает следующее:
F # - петля
; F#
; struct (i, i)
01690a7e 8bce mov ecx,esi
01690a80 8bd6 mov edx,esi
; Loads x (pair) onto stack
01690a82 8d45f0 lea eax,[ebp-10h]
01690a85 83ec08 sub esp,8
01690a88 f30f7e00 movq xmm0,mmword ptr [eax]
01690a8c 660fd60424 movq mmword ptr [esp],xmm0
; Push new tuple on stack
01690a91 52 push edx
01690a92 51 push ecx
; Loads pointer to x into ecx (result will be written here)
01690a93 8d4df0 lea ecx,[ebp-10h]
; Call min
01690a96 ff15744dfe00 call dword ptr ds:[0FE4D74h]
; Increase i
01690a9c 46 inc esi
01690a9d 81fe01e1f505 cmp esi,offset FSharp_Core_ni+0x6be101 (05f5e101)
; Reached the end?
01690aa3 7cd9 jl 01690a7e
С# - цикл
; C#
; Loads x (pair) into ecx, eax
02c2057b 8d55ec lea edx,[ebp-14h]
02c2057e 8b0a mov ecx,dword ptr [edx]
02c20580 8b4204 mov eax,dword ptr [edx+4]
; new System.ValueTuple<int, int>(i, i)
02c20583 8bfe mov edi,esi
02c20585 8bd6 mov edx,esi
; Push x on stack
02c20587 50 push eax
02c20588 51 push ecx
; Push new tuple on stack
02c20589 52 push edx
02c2058a 57 push edi
; Loads pointer to x into ecx (result will be written here)
02c2058b 8d4dec lea ecx,[ebp-14h]
; Call MinPair
02c2058e ff15104d2401 call dword ptr ds:[1244D10h]
; Increase i
02c20594 46 inc esi
; Reached the end?
02c20595 81fe00e1f505 cmp esi,5F5E100h
02c2059b 7ede jle 02c2057b
Трудно понять, почему код F # должен здесь значительно хуже. Код выглядит примерно эквивалентным с исключением того, как x
загружается в стек. До тех пор, пока кто-нибудь не объяснит, почему я собираюсь предположить, что его movq
имеет более низкую задержку, чем push
, и поскольку все инструкции манипулируют стеком, CPU не может изменить порядок инструкций, чтобы уменьшить задержку movq
.
Почему дрожание выбрало movq
для кода F #, а не для кода С#, который я сейчас не знаю.
Для x64 производительность, похоже, ухудшается из-за большего количества накладных расходов в прелюдиях метода и более сглаживания из-за сглаживания. Это главным образом спекуляция с моей стороны, но из кода сборки трудно понять, что, кроме остановки, может снизить производительность x64 в 4 раза.
Помечая min
как встроенный, x64 и x86 работают в ~ 0,15 сек. Неудивительно, что это устраняет все накладные расходы из прелюдий метода и много чтения и записи в стек.
Маркировка методов F # для агрессивной вставки (с помощью [MethodImpl (MethodImplOptions.AggressiveInlining)]
) не работает, поскольку компилятор F # удаляет все такие атрибуты, что означает, что дрожание никогда не видит его, но маркировка методов С# для агрессивной вставки делает код С# запущенным в ~ 0,15 сек.
Итак, в конце концов, джиттер x86 по какой-то причине выбрал jit код по-другому, хотя код IL выглядит очень похоже. Возможно, атрибуты методов влияют на дрожание, поскольку они немного разные.
Фракция x64, вероятно, могла бы улучшить работу по более точной настройке параметров в стеке. Я думаю, используя push
, поскольку джиттер x86 предпочтительнее над mov
, поскольку семантика push
более ограничена, но это только предположение с моей стороны.
В таких случаях, когда методы дешевы, маркировка их как встроенных может быть хорошей.
Честно говоря, я не уверен, что это помогает OP, но, надеюсь, это было несколько интересно.
PS. Я запускаю код на .NET 4.6.2 на i5 3570K