Доклад для Middle и Senior .NET-программистов о микроптимизациях приложения, из которого Вы узнаете:
О том, как важно понимать IL и ASM код, соответствующий вашей C#-программе;
О различных уровнях микрооптимизаций начиная от C# и JIT компиляторов, заканчивая CPU;
Об особенностях оптимизаций под различные процессорные архитектуры;
Об отличиях разных версиях JIT-компиляторов, включая RyuJIT;
О том, как правильно замерять время выполнения приложений и оценивать эффективность оптимизаций.
Доклад будет полезен всем разработчикам, которые хотят хотят сделать свои и без того быстрые программы ещё на 5-10% быстрее.
4. С чего начать?
Хочу, чтобы программа работала быстрее!
• Хорошая архитектура
• Эффективные алгоритмы
• Правильные структуры данных
• Разумное использование памяти
• I/O
• Networking
• Кэш
• ...
3/50 Theory
5. С чего начать?
Хочу, чтобы программа работала быстрее!
• Хорошая архитектура
• Эффективные алгоритмы
• Правильные структуры данных
• Разумное использование памяти
• I/O
• Networking
• Кэш
• ...
Но мы не будем про это разговаривать...
3/50 Theory
7. Под что будем оптимизировать?
• Версия CLR: CLR2? CLR4? CoreCLR? Mono?
• Версия ОС: Windows? Linux? MacOS?
• Версия JIT: x86? x64? RyuJIT?
• Версия GC: MS (какой CLR?)? Mono
(Boehm/Sgen)?
• Компиляция: JIT? NGen? Есть ли MPGO?
.NET Native?
• Разное железо: тысячи его.
4/50 Theory
8. Микробенчмаркинг — это сложно
Чтобы увеличить скорость работы, нужно сначала научиться её
измерять, а для этого:
5/50 Theory
9. Микробенчмаркинг — это сложно
Чтобы увеличить скорость работы, нужно сначала научиться её
измерять, а для этого:
• Запускаем бенчмарки в разных процессах.
• Делаем грамотный прогрев и подсчёт статистик.
• Проверяем разные окружения (и не забываем про RyuJIT).
• Побеждаем сайд-эффекты.
5/50 Theory
10. Микробенчмаркинг — это сложно
Чтобы увеличить скорость работы, нужно сначала научиться её
измерять, а для этого:
• Запускаем бенчмарки в разных процессах.
• Делаем грамотный прогрев и подсчёт статистик.
• Проверяем разные окружения (и не забываем про RyuJIT).
• Побеждаем сайд-эффекты.
• Понимаем устройство .NET, устройство ОС, устройство CPU и
устройство вселенной.
5/50 Theory
11. Микробенчмаркинг — это сложно
Чтобы увеличить скорость работы, нужно сначала научиться её
измерять, а для этого:
• Запускаем бенчмарки в разных процессах.
• Делаем грамотный прогрев и подсчёт статистик.
• Проверяем разные окружения (и не забываем про RyuJIT).
• Побеждаем сайд-эффекты.
• Понимаем устройство .NET, устройство ОС, устройство CPU и
устройство вселенной.
На правах рекламы:
https://github.com/PerfDotNet/BenchmarkDotNet/
https://www.nuget.org/packages/BenchmarkDotNet/
c Andrey Akinshin, Jon Skeet, Matt Warren
5/50 Theory
12. Микробенчмаркинг — это сложно
Чтобы увеличить скорость работы, нужно сначала научиться её
измерять, а для этого:
• Запускаем бенчмарки в разных процессах.
• Делаем грамотный прогрев и подсчёт статистик.
• Проверяем разные окружения (и не забываем про RyuJIT).
• Побеждаем сайд-эффекты.
• Понимаем устройство .NET, устройство ОС, устройство CPU и
устройство вселенной.
На правах рекламы:
https://github.com/PerfDotNet/BenchmarkDotNet/
https://www.nuget.org/packages/BenchmarkDotNet/
c Andrey Akinshin, Jon Skeet, Matt Warren
Сегодня мы будем проводить измерения в попугаях.
5/50 Theory
13. Поговорим про С#-компилятор и IL
You should always understand at least one layer
below what you are coding. c Lee Campbell
6/50 Compiler+IL
14. Поговорим про switch
switch (x)
{
case 0:
return 0;
case 1:
return 1;
case 2:
return 2;
case 3:
return 3;
}
return -1;
7/50 Compiler+IL
15. А что внутри?
; Фаза 1: switch
IL_0008: switch (IL_001f, IL_0021, IL_0023, IL_0025)
IL_001d: br.s IL_0027
; Фаза 2: cases
IL_001f: ldc.i4.0
IL_0020: ret
IL_0021: ldc.i4.1
IL_0022: ret
IL_0023: ldc.i4.2
IL_0024: ret
IL_0025: ldc.i4.3
IL_0026: ret
IL_0027: ldc.i4.m1
IL_0028: ret
8/50 Compiler+IL
16. Поговорим про switch
switch (x)
{
case 0:
return 0;
case 1000:
return 1000;
case 2000:
return 2000;
case 3000:
return 3000;
}
return -1;
9/50 Compiler+IL
17. А что внутри?
; Фаза 1: switch
IL_0007: ldloc.0
IL_0008: ldc.i4 1000
IL_000d: bgt.s IL_001c
IL_000f: ldloc.0
IL_0010: brfalse.s IL_002e
IL_0012: ldloc.0
IL_0013: ldc.i4 1000
IL_0018: beq.s IL_0030
IL_001a: br.s IL_0042
IL_001c: ldloc.0
IL_001d: ldc.i4 2000
IL_0022: beq.s IL_0036
IL_0024: ldloc.0
IL_0025: ldc.i4 3000
IL_002a: beq.s IL_003c
IL_002c: br.s IL_0042
; Фаза 2: cases
IL_002e: ldc.i4.0
IL_002f: ret
IL_0030: ldc.i4 1000
IL_0035: ret
IL_0036: ldc.i4 2000
IL_003b: ret
IL_003c: ldc.i4 3000
IL_0041: ret
IL_0042: ldc.i4.m1
IL_0043: ret
10/50 Compiler+IL
18. Поговорим про switch
switch (x)
{
case "a":
return "A";
case "b":
return "B";
case "c":
return "C";
case "d":
return "D";
}
return "";
11/50 Compiler+IL
19. А что внутри?
; Фаза 1: switch
IL_0007: ldloc.0
IL_0008: ldstr "a"
IL_000d: call bool String::op_Equality
IL_0012: brtrue.s IL_003d
IL_0014: ldloc.0
IL_0015: ldstr "b"
IL_001a: call bool String::op_Equality
IL_001f: brtrue.s IL_0043
IL_0021: ldloc.0
IL_0022: ldstr "c"
IL_0027: call bool String::op_Equality
IL_002c: brtrue.s IL_0049
IL_002e: ldloc.0
IL_002f: ldstr "d"
IL_0034: call bool String::op_Equality
IL_0039: brtrue.s IL_004f
IL_003b: br.s IL_0055
; Фаза 2: cases
IL_003d: ldstr "A"
IL_0042: ret
IL_0043: ldstr "B"
IL_0048: ret
IL_0049: ldstr "C"
IL_004e: ret
IL_004f: ldstr "D"
IL_0054: ret
IL_0055: ldstr ""
IL_005a: ret
12/50 Compiler+IL
20. Поговорим про switch
switch (x)
{
case "a":
return "A";
case "b":
return "B";
case "c":
return "C";
case "d":
return "D";
case "e":
return "E";
case "f":
return "F";
case "g":
return "G";
}
return "";
13/50 Compiler+IL
21. А что внутри?
; Старый компилятор
; Фаза 0: Dictionary<string, string>
IL_000d: volatile.
IL_000f: ldsfld class
Dictionary<string, int32>
IL_0014: brtrue.s IL_0077
IL_0016: ldc.i4.7
IL_0017: newobj instance void class
Dictionary<string, int32>::.ctor
IL_001c: dup
IL_001d: ldstr "a"
IL_0022: ldc.i4.0
IL_0023: call instance void class
Dictionary<string, int32>::Add
IL_0028: dup
IL_0029: ldstr "b"
IL_002e: ldc.i4.1
IL_0023: call instance void class
Dictionary<string, int32>::Add
IL_0034: dup
IL_0035: ldstr "c"
; ...
; Фаза 1:
IL_0088: ldloc.1
IL_0089: switch (
IL_00ac, IL_00b2, IL_00b8,
IL_00be, IL_00c4, IL_00ca, IL_00d0)
IL_00aa: br.s IL_00d6
; Фаза 2: cases
IL_00ac: ldstr "A"
IL_00b1: ret
IL_00b2: ldstr "B"
IL_00b7: ret
IL_00b8: ldstr "C"
IL_00bd: ret
IL_00be: ldstr "D"
IL_00c3: ret
IL_00c4: ldstr "E"
IL_00c9: ret
IL_00ca: ldstr "F"
IL_00cf: ret
IL_00d0: ldstr "G"
IL_00d5: ret
IL_00d6: ldstr ""
IL_00db: ret
14/50 Compiler+IL
22. А что внутри?
// Roslyn
// Фаза 1: ComputeStringHash
uint num = ComputeStringHash(x);
// Фаза 2: Бинарный поиск
if (num <= 3792446982u)
if (num != 3758891744u)
if (num != 3775669363u)
if (num == 3792446982u)
if (text == "g")
return "G";
else if (text == "d")
return "D";
else if (text == "e")
return "E";
else if (num <= 3826002220u)
if (num != 3809224601u)
if (num == 3826002220u)
if (text == "a")
return "A";
else if (text == "f")
return "F";
else if (num != 3859557458u)
if (num == 3876335077u)
if (text == "b")
return "B";
else if (text == "c")
return "C";
return "";
15/50 Compiler+IL
24. Поговорим про Readonly fields
public struct Int256
{
private readonly long bits0, bits1, bits2, bits3;
public Int256(long bits0, long bits1, long bits2, long bits3)
{
this.bits0 = bits0; this.bits1 = bits1;
this.bits2 = bits2; this.bits3 = bits3;
}
public long Bits0 => bits0; public long Bits1 => bits1;
public long Bits2 => bits2; public long Bits3 => bits3;
}
private Int256 a = new Int256(1L, 5L, 10L, 100L);
private readonly Int256 b = new Int256(1L, 5L, 10L, 100L);
[Benchmark] public long GetValue() =>
a.Bits0 + a.Bits1 + a.Bits2 + a.Bits3;
[Benchmark] public long GetReadOnlyValue() =>
b.Bits0 + b.Bits1 + b.Bits2 + b.Bits3;
17/50 Compiler+IL
25. Поговорим про Readonly fields
public struct Int256
{
private readonly long bits0, bits1, bits2, bits3;
public Int256(long bits0, long bits1, long bits2, long bits3)
{
this.bits0 = bits0; this.bits1 = bits1;
this.bits2 = bits2; this.bits3 = bits3;
}
public long Bits0 => bits0; public long Bits1 => bits1;
public long Bits2 => bits2; public long Bits3 => bits3;
}
private Int256 a = new Int256(1L, 5L, 10L, 100L);
private readonly Int256 b = new Int256(1L, 5L, 10L, 100L);
[Benchmark] public long GetValue() =>
a.Bits0 + a.Bits1 + a.Bits2 + a.Bits3;
[Benchmark] public long GetReadOnlyValue() =>
b.Bits0 + b.Bits1 + b.Bits2 + b.Bits3;
LegacyJIT-x64 RyuJIT-x64
GetValue 1 попугай 1 попугай
GetReadOnlyValue 6.2 попугая 7.6 попугая
17/50 Compiler+IL
26. Как же так?
; GetValue
IL_0000: ldarg.0
IL_0001: ldflda valuetype Program::a
IL_0006: call instance int64 Int256::get_Bits0()
; GetReadOnlyValue
IL_0000: ldarg.0
IL_0001: ldfld valuetype Program::b
IL_0006: stloc.0
IL_0007: ldloca.s 0
IL_0009: call instance int64 Int256::get_Bits0()
См. также: Jon Skeet, Micro-optimization: the surprising inefficiency of readonly fields
18/50 Compiler+IL
28. Наши новые друзья
• COMPLUS_JitDump
• COMPLUS_JitDisasm
• COMPLUS_JitDiffableDasm
• COMPLUS_JitGCDump
• COMPLUS_JitUnwindDump
• COMPLUS_JitEHDump
• COMPLUS_JitTimeLogFile
• COMPLUS_JitTimeLogCsv
См. также:
Book of the Runtime, JIT Compiler Structure
20/50 JIT+ASM
29. Array bound check elimination
const int N = 11;
int[] a = new int[N];
for (int i = 0; i < N; i++)
sum += a[i];
VS
for (int i = 0; i < a.Length; i++)
sum += a[i];
21/50 JIT+ASM
30. Array bound check elimination
const int N = 11;
int[] a = new int[N];
for (int i = 0; i < N; i++)
sum += a[i];
VS
for (int i = 0; i < a.Length; i++)
sum += a[i];
LegacyJIT-x86 LegacyJIT-x64 RyuJIT-x64
N 1.3 попугая 1 попугай 1.6 попугая
a.Length 1.2 попугая 1 попугай 1.5 попугая
21/50 JIT+ASM
31. Как же так?
LegacyJIT-x86
; LegacyJIT-x86, N
; ...
Loop:
; if i >= a.Length
cmp eax,edx
; then throw Exception
jae 004635F9
; sum += a[i]
add ecx,dword ptr [esi+eax*4+8]
; i++
inc eax
; if i < N
cmp eax,0Bh
; then continue
jl Loop
; LegacyJIT-x86, a.Length
; ...
; ...
; ...
; ...
; ...
Loop:
; sum += a[i]
add ecx,dword ptr [esi+edx*4+8]
; i++
inc edx
; if i < a.Length
cmp eax,edx
; then continue
jg Loop
22/50 JIT+ASM
32. Как же так?
LegacyJIT-x64
; LegacyJIT-x64, N
; ...
Loop:
; eax = x[i]
mov eax,dword ptr [r8+rcx+10h]
; sum += a[i]
add edx,eax
; i++
add rcx,4
; if i < N
cmp rcx,2Ch
; then continue
jl Loop
; LegacyJIT-x64, a.Length
; ...
Loop:
; eax = x[i]
mov eax,dword ptr [r9+r8+10h]
; sum += a[i]
add ecx,eax
; i++
inc edx
add r8,4
; if i < N
cmp edx,r10d
; then continue
jl Loop
23/50 JIT+ASM
33. Как же так?
RyuJIT-x64
; RyuJIT-x64, N
Loop:
; r8 = a.Length
mov r8d,dword ptr [rax+8]
; if i >= a.Length
cmp ecx,r8d
; then throw exception
jae 00007FF94F9B46B4
; r8 = i
movsxd r8,ecx
; r8 = a[i]
mov r8d,dword ptr [rax+r8*4+10h]
; sum += a[i]
add edx,r8d
; i++
inc ecx
; if i < N
cmp ecx,0Bh
; then continue
jl Loop
; RyuJIT-x64, a.Length
; ...
; ...
; ...
; ...
; ...
; ...
Loop:
; r9 = i
movsxd r9,ecx
; r9 = x[i]
mov r9d,dword ptr [rax+r9*4+10h]
; sum += x[i]
add edx,r9d
; i++
inc ecx
; if i < a.Length
cmp r8d,ecx
; then continue
jg Loop
24/50 JIT+ASM
39. Как же так?
LegacyJIT-x64
; LegacyJIT-x64
mov ecx,23h
ret
RyuJIT-x64
// Inline expansion aborted due to opcode
// [06] OP_starg.s in method
// Program:WithStarg(int):int:this
28/50 JIT+ASM
42. Loop unrolling
int sum = 0;
for (int i = 0; i < 1024; i++)
sum += i;
LegacyJIT-x86 LegacyJIT-x64 RyuJIT-x64
1.0 попугай 0.9 попугая 1.0 попугай
30/50 JIT+ASM
43. Как же так?
LegacyJIT-x64
xor ecx,ecx
mov edx,1
Loop:
lea eax,[rdx-1]
add ecx,eax ; sum += (i)
add ecx,edx ; sum += (i+1)
lea eax,[rdx+1]
add ecx,eax ; sum += (i+2)
lea eax,[rdx+2]
add ecx,eax ; sum += (i+3)
add edx,4 ; i += 4
cmp edx,401h
jl Loop
31/50 JIT+ASM
65. Задачка
private double[] x = new double[11];
[Benchmark]
public double Calc()
{
double sum = 0.0;
for (int i = 1; i < x.Length; i++)
sum += 1.0 / (i * i) * x[i];
return sum;
}
47/50 Parallelism
66. Задачка
private double[] x = new double[11];
[Benchmark]
public double Calc()
{
double sum = 0.0;
for (int i = 1; i < x.Length; i++)
sum += 1.0 / (i * i) * x[i];
return sum;
}
LegacyJIT-x64 RyuJIT-x641
Calc 1 попугай 2 попугая
1
RyuJIT RC
47/50 Parallelism