Есть 2 примера ассемблера(ниже). и еще 2 кастомных.
1. 1 чуть оптимизированный код дизасеблера c#
2. код полученный из gcc -o2 (-o3 уже векторизует) он медленее (хотя я рассчитывал на обратное).
Пример простая функция, которая находит в массиве макс и мин значения.
1 пример, чуть улучшенный jit asm код c# clr метода. Тут тоже вопрос, CLR если проверите в diasm генерит безусловные переходы под return с 2 goto c возвратом на строчку ниже, когда может заинлайнить(сджойнить) по всем законам оптимизацией базовых блоков. (в самом низу дизасемблер, там прыжки вниз вверх)
2 пример.
Тут вопрос, ожидал ускорение, получил наоборот (c++ gcc o2 компилятор такой код выводит). Функция без переходов, используются условные присваивания без переходов по паттерну x>y?x:y; Он медленнее у меня в 1.3 раза. Почему вроде и код короче, и код цикла меньше. И нету условных переходов, и вроде как
При этом все clr методы на данных 1-4 размером медленнее в 3-4 раза!!! Опять же почему! В чем разница, например код cгенерированный iced Assembler он почти аналогичен (чуть оптимизирован как писал). Откуда разница? Я ожидал, что Вызов clr вызов должен быть быстрее так как там прямой call по адресу, а в сгенерированных по указателю, и там еще всякие Gc.
Но самое непонятное. почему обычный код с передачей int[]arr в функцию, быстрее прям заметно всех, ведь там тоно такой же код
И как вообще отлаживать такой код, почему Visual stuio дебагер не переходит в динамически скомпилированные методы, а пропускает их.
Из интересного, вопросы,
-зачем кладется регистр Rax на стек в прологах функций? Какой смысл.
-Почему clr генерит возврат структуры, через стек, кладет на стек поля структуры(с смещением 04), и потом считывает, вместо сдвига на регистре.
- почему и как так получается, что код для min max генерится совершенно другой, нежели код самих функций min max по отдельности, откуда ветвление появляется, там(в реализации min) код с условным присваиванием как в более медленном примере.
- Где и как происходит раскрутка циклов, она же там где-то есть вообще, я ни разу не видел???? Как получить этот самый код, (но я тестировал если раскрыть в ассемблере, то опять же быстрее не получится), значит там что--то другое.
| Method | n | Mean | Error | StdDev |
|---------------------- |------ |--------------:|-----------:|------------:|
| testMinMax_AsmJit | 1 | 0.8162 ns
| testMinMax_cpp_O2 | 1 | 0.7975 ns
| testCLR | 1 | 4.5242 ns
| testCLR_arr_delgegate | 1 | 4.2673 ns
| testCLR_arrays | 1 | 4.1436 ns
| testMinMax_AsmJit | 4 | 2.7270 ns
| testMinMax_cpp_O2 | 4 | 2.5292 ns
| testCLR | 4 | 6.4733 ns
| testCLR_arr_delgegate | 4 | 7.0194 ns
| testCLR_arrays | 4 | 5.2367 ns
| testMinMax_AsmJit | 10 | 8.6922 ns
| testMinMax_cpp_O2 | 10 | 4.3692 ns
| testCLR | 10 | 11.3411 ns
| testCLR_arr_delgegate | 10 | 12.8448 ns
| testCLR_arrays | 10 | 10.2702 ns
| testMinMax_AsmJit | 10000 | 4,008.1698 n
| testMinMax_cpp_O2 | 10000 | 4,995.2740 ns
| testCLR | 10000 | 4,402.3739 ns
| testCLR_arr_delgegate | 10000 | 3,717.5476 ns
| testCLR_arrays | 10000 | 3,359.1073 ns
Код бенчмарка с библиотекой Iced.Intel (надеюсь правильно скопировал) класс ExecuteAllocator c VirtualAlloc MemoryProtection.ExecuteReadWrite Пускай нейросеть сгенерит(в лимит вопроса не влез)
using Iced.Intel;
using static Iced.Intel.AssemblerRegisters;
public unsafe class Test
{
nint ptrCode1;
nint ptrCode2;
[Params(1,4,10, 10_000)]
public int n = 10000;
private int[] arr;
unsafe delegate minmax MinMaxFunc(int* ptr, int len);
private unsafe MinMaxFunc nativeFunc;
private unsafe Func<int[],int,minmax> nativeFuncArr;
private unsafe delegate*<int*, int, minmax> funcBranch;
private unsafe delegate*<int*, int, minmax> funcNoBranch;
[GlobalSetup]
public void GlobalSetup()
{
// чат gpt сгенерит
ptrCode1 = ExecuteAllocator.Alloc(4096);
ptrCode2 = ExecuteAllocator.Alloc(4096);
Assembler asm1 = Program.createcodeMinMax01();
Assembler asm2 = Program.createcodeMinMax02();
var stream1 = new UnmanagedMemoryStream((byte*)ptrCode1, 1024 * 4, 1024 * 4, FileAccess.ReadWrite);
var stream2 = new UnmanagedMemoryStream((byte*)ptrCode2, 1024 * 4, 1024 * 4, FileAccess.ReadWrite);
asm1.Assemble(new StreamCodeWriter(stream1), 0);
asm2.Assemble(new StreamCodeWriter(stream2), 0);
Random rnd = new Random(1);
arr = Enumerable.Range(0, n).Select(i => rnd.Next(99999)).ToArray();
funcBranch = (delegate* managed<int*, int, minmax>)ptrCode1;
funcNoBranch = (delegate* managed<int*, int, minmax>)ptrCode2;
nativeFunc = test;
nativeFuncArr = test;
}
[GlobalCleanup]
public void GlobalCleanup()
{
ExecuteAllocator.Free(ptrCode1);
ExecuteAllocator.Free(ptrCode2);
}
[Benchmark]
public unsafe minmax testMinMax_AsmJit()
{
return funcBranch((int*)Unsafe.AsPointer(ref arr[0]), n);
}
[Benchmark]
public unsafe minmax testMinMax_cpp_O2()
{
return funcNoBranch((int*)Unsafe.AsPointer(ref arr[0]), n);
}
[Benchmark]
public unsafe minmax testCLR()
{
return nativeFunc((int*)Unsafe.AsPointer(ref arr[0]), n);
}
[Benchmark]
public unsafe minmax testCLR_arr_delgegate()
{
return nativeFuncArr(arr, n);
}
}
public static unsafe minmax test(int* arr, int len)
{
int min = int.MaxValue;
int max = 0;
for (int i = 0; i < len; i++)
{
int t = arr[i];
min = int.Min(min, t);
max = int.Max(max, t);
}
return new minmax(min, max);
}
public record struct minmax(int min, int max);
private static unsafe Assembler createcodeMinMax01()
{
Assembler asm = new Assembler(64);
var endLoop = asm.CreateLabel();
var begLoop = asm.CreateLabel();
var l51 = asm.CreateLabel();
var l53 = asm.CreateLabel();
asm.push(rax); // А Зачем Регистр rax Caller хз!!!!!!!!!
asm.mov(r8d, 0x7FFFFFFF);
asm.xor(r10d, r10d);
asm.xor(r9d, r9d);
asm.test(edx, edx);
asm.jle(endLoop);
// Выравнивание (nop) зачем, ладно оставлю(надеюсь не в этом причины)
asm.nop();
asm.nop();
// Основной цикл
asm.Label(ref @begLoop);
asm.movsxd(rax, r9d);
asm.mov(r11d, __[rcx + rax * 4]); /// самое гениальное переопределение операторов +* прям в учебники
asm.cmp(r10d, r11d);
asm.jg(l53);
asm.mov(r10d, r11d);
asm.Label(ref @l53);
asm.cmp(r8d, r11d);
asm.jl(l51);
asm.mov(r8d, r11d);
asm.Label(ref l51);
asm.inc(r9d);
asm.cmp(r9d, edx);
asm.jl(@begLoop);
asm.Label(ref endLoop);
// jitasm генерит ересь тута (возможно как раз просадка в 3 раза на данных 1-4 только что понял)
// зачем на стек кладет и читает.
asm.shl(r10, 32);
asm.or(r10, r8);
asm.add(rsp, 8); ///зачем вообще rax клался на стек, если убрать(и push) ни чего не сломается
asm.mov(rax, r10);
asm.ret();
return asm;
}
private static unsafe Assembler createcodeMinMax02( )
{
Assembler asm = new Assembler(64);
{
var end=asm.CreateLabel("@end");
var beg=asm.CreateLabel("@beg");
asm.mov(rax, 0x7FFFFFFF);
asm.test(edx, edx);
asm.jle(@end);
asm.lea(rsi, __[rcx+rdx*4]); //last elem
asm.mov(rdi, rcx); //iterator elem
asm.xor(rcx, rcx);
asm.Label(ref @beg);
asm.mov(edx, __[rdi]);
asm.cmp(eax, edx);
asm.cmovg(eax, edx);
asm.cmp(ecx, edx);
asm.cmovl(ecx, edx);
asm.add(rdi, 4);
asm.cmp(rdi,rsi);
asm.jne(@beg);
asm.Label(ref @end);
asm.shl(rcx, 32);
asm.or(rax, rcx);
asm.ret();
}
return asm;
}
G_M000_IG01: ;; offset=0x0000
push rax
G_M000_IG02: ;; offset=0x0001
mov r8d, 0x7FFFFFFF
xor r10d, r10d
xor r9d, r9d
test edx, edx
jle SHORT G_M000_IG06
align [15 bytes for IG03]
G_M000_IG03: ;; offset=0x0020
movsxd rax, r9d
mov r11d, dword ptr [rcx+4*rax]
cmp r8d, r11d
jg SHORT G_M000_IG08
G_M000_IG04: ;; offset=0x002C
cmp r10d, r11d
jl SHORT G_M000_IG09
G_M000_IG05: ;; offset=0x0031
inc r9d
cmp r9d, edx
jl SHORT G_M000_IG03
G_M000_IG06: ;; offset=0x0039
mov dword ptr [rsp], r8d
mov dword ptr [rsp+0x04], r10d
mov rax, qword ptr [rsp]
G_M000_IG07: ;; offset=0x0046
add rsp, 8
ret
G_M000_IG08: ;; offset=0x004B
mov r8d, r11d
jmp SHORT G_M000_IG04
G_M000_IG09: ;; offset=0x0050
mov r10d, r11d
jmp SHORT G_M000_IG05