Задать вопрос
@SergeySerge11

Почему код без условных переходов медленнее чем с ними? Ассемблер. Если должно быть наоборот?

Есть 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
  • Вопрос задан
  • 55 просмотров
Подписаться 1 Простой 5 комментариев
Пригласить эксперта
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Похожие вопросы