Borland Assembler (BASM) уроки для начинающих

Тема в разделе "WASM.ARTICLES", создана пользователем Mikl___, 23 дек 2016.

  1. Mikl___

    Mikl___ Супермодератор Команда форума

    Публикаций:
    14
    Регистрация:
    25 июн 2008
    Сообщения:
    3.792
    Код (Pascal):
    1. function ArcSinApprox3c(X, A, B, C, D : Double) : Double;
    2. asm
    3. //push ebp
    4. //mov ebp,esp
    5. add  esp,-8
    6. //Result := ((A*X + B)*X + C)*X + D;
    7. fld  qword ptr [ebp+20h]
    8. fmul qword ptr [ebp+28h]
    9. fadd qword ptr [ebp+18h]
    10. fmul qword ptr [ebp+28h]
    11. fadd qword ptr [ebp+10h]
    12. fmul qword ptr [ebp+28h]
    13. fadd qword ptr [ebp+8]
    14. fstp qword ptr [ebp-8]
    15. wait
    16. fld  qword ptr [ebp-8]
    17. pop  ecx
    18. pop  ecx
    19. //pop ebp
    20. end;
    Первым делом удаляем строку ADD ESP,-8 и две строки POP ECX. Они устанавливают фрейм стека, но ничего не делают кроме манипулирования указателем стека, который нигде не используется.
    Код (Pascal):
    1. function ArcSinApprox3d(X, A, B, C, D : Double) : Double;
    2. asm
    3. //add  esp,-8
    4. //Result := ((A*X + B)*X + C)*X + D;
    5. fld  qword ptr [ebp+20h]
    6. fmul qword ptr [ebp+28h]
    7. fadd qword ptr [ebp+18h]
    8. fmul qword ptr [ebp+28h]
    9. fadd qword ptr [ebp+10h]
    10. fmul qword ptr [ebp+28h]
    11. fadd qword ptr [ebp+8]
    12. fstp qword ptr [ebp-8]
    13. wait
    14. fld  qword ptr [ebp-8]
    15. //pop  ecx
    16. //pop  ecx
    17. end;
    Данная функция получила 43535 пункта.
    Обе лишние строки, копирующие результат на стек и обратно, удалены одновременно.
    Код (Pascal):
    1. function ArcSinApprox3e(X, A, B, C, D : Double) : Double;
    2. asm
    3. //Result := ((A*X + B)*X + C)*X + D;
    4. fld  qword ptr [ebp+20h]
    5. fmul qword ptr [ebp+28h]
    6. fadd qword ptr [ebp+18h]
    7. fmul qword ptr [ebp+28h]
    8. fadd qword ptr [ebp+10h]
    9. fmul qword ptr [ebp+28h]
    10. fadd qword ptr [ebp+8]
    11. //fstp qword ptr [ebp-8]
    12. wait
    13. //fld  qword ptr [ebp-8]
    14. end;
    Этот вариант получил 47237 пункта, и улучшение составило 8.5%
    Затем изменим код, таким образом, чтобы X загружался только один раз.
    Код (Pascal):
    1. function ArcSinApprox3f(X, A, B, C, D : Double) : Double;
    2. asm
    3. //Result := ((A*X + B)*X + C)*X + D;
    4. fld  qword ptr [ebp+20h]
    5. fld  qword ptr [ebp+28h]
    6. fxch
    7. //fmul qword ptr [ebp+28h]
    8. fmul  st(0),st(1)
    9. fadd  qword ptr [ebp+18h]
    10. //fmul qword ptr [ebp+28]
    11. fmul  st(0),st(1)
    12. fadd  qword ptr [ebp+10h]
    13. //fmul qword ptr [ebp+28h]
    14. fmul  st(0),st(1)
    15. ffree st(1)
    16. fadd qword ptr [ebp+8]
    17. wait
    18. end;
    Данная функция получила 47226 пункта и без изменения производительности.
    Инструкция FFREE может быть удалена, за счет использования инструкции FMULP вместо FMUL, но для этого мы должны сменить два используемых регистра. Только эти два регистра используются и A*B = B*A, так что нет проблем сделать это. Этим мы не удаляем некоторую избыточность, и оба пути дают одинаковый результат.
    Код (Pascal):
    1. function ArcSinApprox3g(X, A, B, C, D : Double) : Double;
    2. asm
    3. //Result := ((A*X + B)*X + C)*X + D;
    4. fld  qword ptr [ebp+20h]
    5. fld  qword ptr [ebp+28h]
    6. fxch  st(1)
    7. fmul  st(0),st(1)
    8. fadd  qword ptr [ebp+18h]
    9. fmul  st(0),st(1)
    10. fadd  qword ptr [ebp+10h]
    11. //fmul  st(0),st(1)
    12. fmulp st(1),st(0)
    13. //ffree st(1)
    14. fadd qword ptr [ebp+8]
    15. wait
    16. end;
    Данная реализация получила 47416 пункта.
    Затем мы удалим инструкцию WAIT.
    Код (Pascal):
    1. function ArcSinApprox3h(X, A, B, C, D : Double) : Double;
    2. asm
    3. //Result := ((A*X + B)*X + C)*X + D;
    4. fld  qword ptr [ebp+20h]
    5. fld  qword ptr [ebp+28h]
    6. fxch  st(1)
    7. fmul  st(0),st(1)
    8. fadd  qword ptr [ebp+18h]
    9. fmul  st(0),st(1)
    10. fadd  qword ptr [ebp+10h]
    11. fmulp st(1),st(0)
    12. fadd qword ptr [ebp+8]
    13. //wait
    14. end;
    Теперь функция получила 47059 пункта.
    Последняя вещь, которую мы сделаем, это строки, производящие загрузку X и A, и удалим инструкцию FXCH.
    Код (Pascal):
    1. function ArcSinApprox3i(X, A, B, C, D : Double) : Double;
    2. asm
    3. //Result := ((A*X + B)*X + C)*X + D;
    4. fld  qword ptr [ebp+28h]
    5. fld  qword ptr [ebp+20h]
    6. //fld  qword ptr [ebp+28h]
    7. //fxch  st(1)
    8. fmul  st(0),st(1)
    9. fadd  qword ptr [ebp+18h]
    10. fmul  st(0),st(1)
    11. fadd  qword ptr [ebp+10h]
    12. fmulp st(1),st(0)
    13. fadd qword ptr [ebp+8]
    14. end;
    Эта реализация функции получила 46544 и производительность упала!
    Теперь сравним производительность по версии Хорнера с функцией, получившей наибольшую производительность на P4.
    ArcSinApprox1g 47939
    ArcSinApprox3g 47416
    На P3
    ArcSinApprox1h 85361
    ArcSinApprox3h 87604
    Различие не большое, но обычная функция немного быстрее на P4 и медленнее на P3. Обычная функция имеет больше вычисление, но параллелизм это сгладил. Вариант Хорнера имеющий маленький параллелизм и латентность проявляется в полной мере. Это особо плохо на P4.
    Держим это в уме и продолжаем с третьим решением, которое выглядит так.
    Код (Pascal):
    1. function ArcSinApprox4b(X, A, B, C, D : Double) : Double;
    2. begin
    3. {
    4. push  ebp
    5. mov  ebp,esp
    6. add  esp,-8
    7. }
    8. Result := (A*X + B)*(X*X)+(C*X + D);
    9. {
    10. fld  qword ptr [ebp+20h]
    11. fmul  qword ptr [ebp+28h]
    12. fadd  qword ptr [ebp+18h]
    13. fld  qword ptr [ebp+28h]
    14. fmul  qword ptr [ebp+28h]
    15. fmulp st(1)
    16. fld  qword ptr [ebp+10h]
    17. fmul  qword ptr [ebp+28h]
    18. fadd  qword ptr [ebp+8]
    19. faddp st(1)
    20. fstp  qword ptr [ebp-8]
    21. wait
    22. fld  qword ptr [ebp-8]
    23. }
    24. {
    25. pop ecx
    26. pop ecx
    27. pop ebp
    28. }
    29. end;
    Опыт уже позволяет нам сделать это просто и быстро :)
    Данная версия сделана так, как это сделала Delphi
    Код (Pascal):
    1. function ArcSinApprox4c(X, A, B, C, D : Double) : Double;
    2. asm
    3. //push ebp
    4. //mov ebp,esp
    5. add  esp,-$08
    6. //Result := (A*X + B)*(X*X)+(C*X + D);
    7. fld  qword ptr [ebp+20h]
    8. fmul  qword ptr [ebp+28h]
    9. fadd  qword ptr [ebp+18h]
    10. fld  qword ptr [ebp+28h]
    11. fmul  qword ptr [ebp+28h]
    12. fmulp //st(1)
    13. fld  qword ptr [ebp+10h]
    14. fmul  qword ptr [ebp+28h]
    15. fadd  qword ptr [ebp+8]
    16. faddp //st(1)
    17. fstp  qword ptr [ebp-8]
    18. wait
    19. fld  qword ptr [ebp-8]
    20. pop  ecx
    21. pop  ecx
    22. //pop  ebp
    23. end;
    Удаляем фрейм стека и две строки, которые пишут результат на стек
    Код (Pascal):
    1. function ArcSinApprox4d(X, A, B, C, D : Double) : Double;
    2. asm
    3. //add  esp,-$08
    4. //Result := (A*X + B)*(X*X)+(C*X + D);
    5. fld  qword ptr [ebp+20h]
    6. fmul qword ptr [ebp+28h]
    7. fadd qword ptr [ebp+18h]
    8. fld  qword ptr [ebp+28h]
    9. fmul qword ptr [ebp+28h]
    10. fmulp //st(1)
    11. fld  qword ptr [ebp+10h]
    12. fmul qword ptr [ebp+28h]
    13. fadd qword ptr [ebp+8]
    14. faddp //st(1)
    15. //fstp qword ptr [ebp-8]
    16. wait
    17. //fld  qword ptr [ebp-$08]
    18. //pop  ecx
    19. //pop  ecx
    20. end;
    Загружаем X только раз
    Код (Pascal):
    1. function ArcSinApprox4e(X, A, B, C, D : Double) : Double;
    2. asm
    3. //Result := (A*X + B)*(X*X)+(C*X + D);
    4. fld  qword ptr [ebp+$20]
    5. fld  qword ptr [ebp+$28]
    6. //fmul qword ptr [ebp+$28]
    7. fxch
    8. fmul  st(0),st(1)
    9. fadd  qword ptr [ebp+$18]
    10. //fld  qword ptr [ebp+$28]
    11. fld  st(1)
    12. //fmul  qword ptr [ebp+$28]
    13. fmul  st(0),st(2)
    14. fmulp
    15. fld  qword ptr [ebp+$10]
    16. //fmul  qword ptr [ebp+$28]
    17. fmul  st(0),st(2)
    18. fadd  qword ptr [ebp+$08]
    19. faddp
    20. ffree st(1)
    21. wait
    22. end;
    Удаляем FXCH и WAIT.
    Код (Pascal):
    1. function ArcSinApprox4f(X, A, B, C, D : Double) : Double;
    2. asm
    3. //Result := (A*X + B)*(X*X)+(C*X + D);
    4. fld  qword ptr [ebp+$28]
    5. fld  qword ptr [ebp+$20]
    6. //fxch
    7. fmul  st(0),st(1)
    8. fadd  qword ptr [ebp+$18]
    9. fld  st(1)
    10. fmul  st(0),st(2)
    11. fmulp
    12. fld  qword ptr [ebp+$10]
    13. fmul  st(0),st(2)
    14. fadd  qword ptr [ebp+$08]
    15. faddp
    16. ffree st(1)
    17. //wait
    18. end;
    Переопределяем FFREE ST(1)
    Код (Pascal):
    1. function ArcSinApprox4g(X, A, B, C, D : Double) : Double;
    2. asm
    3. //Result := (A*X + B)*(X*X)+(C*X + D);
    4. fld  qword ptr [ebp+$28]
    5. fld  qword ptr [ebp+$20]
    6. fmul  st(0),st(1)
    7. fadd  qword ptr [ebp+$18]
    8. fld  st(1)
    9. fmul  st(0),st(2)
    10. fmulp
    11. fld  qword ptr [ebp+$10]
    12. fmul  st(0),st(2)
    13. ffree st(2)
    14. fadd  qword ptr [ebp+8]
    15. faddp
    16. //ffree st(1)
    17. end;
    заменяем FMUL/FFREE на FMULP
    Код (Pascal):
    1. function ArcSinApprox4h(X, A, B, C, D : Double) : Double;
    2. asm
    3. //Result := (A*X + B)*(X*X)+(C*X + D);
    4. fld  qword ptr [ebp+$28]
    5. fld  qword ptr [ebp+$20]
    6. fmul  st(0),st(1)
    7. fadd  qword ptr [ebp+$18]
    8. fld  st(1)
    9. fmul  st(0),st(2)
    10. fmulp
    11. fld  qword ptr [ebp+$10]
    12. //fmul  st(0),st(2)
    13. fmulp st(2),st(0)
    14. //ffree st(2)
    15. fadd  qword ptr [ebp+8]
    16. faddp
    17. end;
    Очищаем код и видим, что компилятор еще использует EBP и излишне модифицирует ESP.
    Код (Pascal):
    1. function ArcSinApprox4i(X, A, B, C, D : Double) : Double;
    2. asm
    3. //Result := (A*X + B)*(X*X)+(C*X + D);
    4. fld  qword ptr [ebp+28h]
    5. fld  qword ptr [ebp+20h]
    6. fmul  st(0),st(1)
    7. fadd  qword ptr [ebp+18h]
    8. fld  st(1)
    9. fmul  st(0),st(2)
    10. fmulp
    11. fld  qword ptr [ebp+10h]
    12. fmulp st(2),st(0)
    13. fadd  qword ptr [ebp+8]
    14. faddp
    15. end;
    Теперь большой вопрос, насколько хорошо эта функция работает.
    ArcSinApprox4a 45228
    ArcSinApprox4b 45239
    ArcSinApprox4c 45228
    ArcSinApprox4d 51813
    ArcSinApprox4e 49044
    ArcSinApprox4f 48674
    ArcSinApprox4g 48852
    ArcSinApprox4h 44914
    ArcSinApprox4i 44914
    Мы видим, что в результате «optimizations» на шагах от d до i мы получили «оптимизацию наоборот» на P4, исключая шаг g.
    На P3
    ArcSinApprox4a 68871
    ArcSinApprox4b 68871
    ArcSinApprox4c 68634
    ArcSinApprox4d 86806
    ArcSinApprox4e 85727
    ArcSinApprox4f 83542
    ArcSinApprox4g 80548
    ArcSinApprox4h 88378
    ArcSinApprox4i 85324
     
    Последнее редактирование: 26 дек 2016
  2. Mikl___

    Mikl___ Супермодератор Команда форума

    Публикаций:
    14
    Регистрация:
    25 июн 2008
    Сообщения:
    3.792
    Мы видим, что оптимизационные шаги d и h очень хороши, а шаги e, f g и I плохие. Вполне возможно, что оптимальной реализации нет. Мы можем выбрать вариант h и удалить оставшиеся и просто сделать несколько вариантов и это путь к быстрой оптимизации.
    Так какая же функция победитель? Чтобы найти его мы выберем самую быструю реализацию по каждому решению
    На P4
    ArcSinApprox1f 47939
    ArcSinApprox3g 47416
    ArcSinApprox4d 51813
    Последняя версия самая быстрая. Параллелизм очень важен на современных процессорах и версия 4 бьет остальных на 9%.
    На P3
    ArcSinApprox1h 85361
    ArcSinApprox3h 87604
    ArcSinApprox4h 88378
    Версия 4 победитель на P3, но с меньшим преимуществом.
    Процессор P4 имеет набор инструкций SSE2, который содержит инструкции для точных расчетов с плавающей запятой. Главная идея этих инструкций The в данном наборе – это использование SIMD расчетов. SIMD - это аббревиатура для Single Instruction Multiple Data. «множество данных» (Multiple data) здесь это переменные двойной точности с плавающей запятой (64 bit) и две переменные этих данных могут быть сложены, вычтены, умножены или поделены одной инструкцией. В SSE2 также есть несколько инструкций для скалярных вычислений, которые вычисляют пару этих данных, подобно обычным данным с плавающей запятой на FPU. Наибольшая разница между обычной математикой с плавающей запятой и SSE2 скалярной математикой, в том, что математика с плавающей запятой выполняется на расширенной точности и результат округляется до двойной точности, при копировании в переменную двойной точности в RAM/кэш. Математика SSE2 двойной точности и регистры также двойной точности. Код примеров в данном уроке выполняет несколько вычислений и точность FPU двойная. Если мы загрузим данные, выполним все вычисления и запишем результат, то результат будет только немного меньше, чем при расширенной точности, пока он еще на стеке FPU, и будет округлен до двойной точности, при копировании в переменную. SSE2 вычисления с другой стороны менее точные, в регистре результат также менее точный. При одном вычислении результат будет двойной точности, но когда мы выполним серию вычислений, то накопленная ошибка будет значительно больше. Поскольку FPU выполняет все вычисления с расширенной точностью и хранит промежуточные результаты в регистрах, то можно выполнить много вычислений, прежде чем ошибка станет значимой, ниже двойной точности.
    Мы видим, что точность SSE2 равная двойной или менее точности двойной точности для IA32 плавающей запятой. В чем же преимущество? Есть два преимущества. Регистры не размещаются на стеке, что делает более простым управление кодом и второе то, что вычисления с двойной точностью быстрее, чем с расширенной точностью. Мы должны выбрать скалярные инструкции SSE2, чтобы иметь меньшую латентность, чем для IA32.
    Fadd latency is 5
    Fsub latency is 5
    Fmul latency is 7
    Fp latency is 38

    Addsd latency is 4
    Subsd latency is 4
    Mulsd
    psd latency is 35
    Руководство по оптимизации P4 не имеет данных по латентности и по throughput для инструкции MULSD!
    Мы видим, что латентность меньше на один такт для скаляров SSE2 в основном, и на 3 такта для деления.
    Показатели для Throughput (в случае срабатывания конвейера) следующие
    Fadd throughput is 1
    Fsub throughput is 1
    Fmul throughput is 2
    Fp throughput is 38

    Addsd throughput is 2
    Subsd throughput is 2
    Mulsd
    psd latency is 35
    Здесь мы видим сюрприз для ADDSD и SUBSD, результат в два раза хуже, по сравнению с FADD и Fsub.
    Все, что можно подумать про SSE2, это то, что оно для встраиваемого оборудования, и то, что SIMD вычисления двух наборов данных в параллель просто удлиняет ваши руки!
    Из руководства “Optimizations for Intel P4 and Intel Xeon” таблицы латентности и throughput на странице C-1 показывают, что все инструкции с плавающей запятой SSE2 выполняются на том же конвейере, что и старые инструкции с плавающей запятой. Это означает, что SIMD сложение из примера генерирует две микроинструкции, которые выполняются в конвейере F_ADD. На первом такте число номер 1 вводится в конвейер, а на втором такте вводится число номер 2. поскольку латентность составляет 4 такта первое число покидает конвейер на такте 3, а второе число на такте 4. Это заставляет нас считать, что скалярное сложение SSE2 должно генерировать латентность в 3 такта и throughput в 1 такт. Из этих таблиц кажется, что SIMD версия ADD, ADDPD, имеет туже самую латентность и throughput, как и скалярная версия ADDSD. Или же здесь ошибка в таблицах, или скалярные инструкции также генерируют две микроинструкции, одна из которых «скрытая», и не имеет эффекта. Обращайтесь к Интел!
    Для проверки чисел из таблицы мы создадим некоторый специальный код и померим инструкции.
    Код (Pascal):
    1. procedure TMainForm.BenchmarkADDSDLatency;
    2. var
    3. RunNo, ClockFrequency : Cardinal;
    4. StartTime, EndTime, RunTime : TDateTime;
    5. NoOfClocksPerRun, RunTimeSec : Double;
    6. const
    7. ONE : Double = 1;
    8. NOOFINSTRUCTIONS : Cardinal = 895;
    9.  
    10. begin
    11. ADDSDThroughputEdit.Text := 'Running';
    12. ADDSDThroughputEdit.Color := clBlue;
    13. Update;
    14. StartTime := Time;
    15. for RunNo := 1 to MAXNOOFRUNS do
    16. begin
    17.   asm
    18.   movsd xmm0, ONE
    19.   movsd xmm1, xmm0
    20.   movsd xmm2, xmm0
    21.   movsd xmm3, xmm0
    22.   movsd xmm4, xmm0
    23.   movsd xmm5, xmm0
    24.   movsd xmm6, xmm0
    25.   movsd xmm7, xmm0
    26.  
    27.   addsd xmm0, xmm1
    28.   addsd xmm0, xmm1
    29.   addsd xmm0, xmm1
    30.   addsd xmm0, xmm1
    31.   addsd xmm0, xmm1
    32.   addsd xmm0, xmm1
    33.   addsd xmm0, xmm1
    34.  
    35.   //Repeat the addsd block of code such that there are 128 blocks
    36.  
    37.   end;
    38. end;
    39. EndTime := Time;
    40. RunTime := EndTime - StartTime;
    41. RunTimeSec := (24 * 60 *60 * RunTime);
    42. ClockFrequency := StrToInt(ClockFrequencyEdit.Text);
    43. NoOfClocksPerRun := (RunTimeSec / MaxNoOfRuns) * ClockFrequency * 1000000 /
    44.   NOOFINSTRUCTIONS;
    45. ADDSDThroughputEdit.Text := FloatToStrF(NoOfClocksPerRun, ffFixed, 9, 1);
    46. ADDSDThroughputEdit.Color := clLime;
    47. Update;
    48. end;
    Все инструкции ADDSD оперируют на тех же самых двух регистрах и поэтому они не могут быть выполнены параллельно. Вторая инструкция должна ждать окончания первой, поэтому будет задействована полная латентность.
    Для измерения производительности throughput вставим данный блок 128 раз
    Код (ASM):
    1. addsd xmm1, xmm0
    2. addsd xmm2, xmm0
    3. addsd xmm3, xmm0
    4. addsd xmm4, xmm0
    5. addsd xmm5, xmm0
    6. addsd xmm6, xmm0
    7. addsd xmm7, xmm0
    здесь нет зависимости от данных, и они могут быть выполнены параллельно. Xmm0 используется как источник данных в каждой строке, но это строка не создает зависимости данных.
    Результаты прогона данного кода показывают, что латентность равна 4 тактам, а throughput равна двум тактам. Это соответствует цифрам из таблицы.
    Закодируем три функции для скаляров SSE2 и выполним измерения. Восемь регистров SSE2 называются как XMM0-XMM7, и Delphi не имеет возможности показать их в окне просмотра регистров. Поэтому мы должны создать свой собственный просмотр, созданием глобальной (или локальной) переменной для каждого регистра, поместить его в окно просмотра (watch window) и добавить функцию для копирования содержимого в переменные. Это несколько неудобно и я с надеждой смотрю в сторону Борланд, по созданию окна просмотр XMM регистров. Данный код показывает, как Я сделал это.
    Код (Pascal):
    1. var
    2. XMM0reg, XMM1reg, XMM2reg, XMM3reg, XMM4reg : Double;
    3.  
    4. function ArcSinApprox3i(X, A, B, C, D : Double) : Double;
    5. asm
    6. //Result := ((A*X + B)*X + C)*X + D;
    7.  
    8. fld  qword ptr [ebp+$20]
    9. movsd xmm0,qword ptr [ebp+$20]
    10.  
    11. movsd XMM0reg,xmm0
    12. movsd XMM1reg,xmm1
    13. movsd XMM2reg,xmm2
    14. movsd XMM3reg,xmm3
    15.  
    16. fld  qword ptr [ebp+$28]
    17. movsd xmm1,qword ptr [ebp+$28]
    18.  
    19. movsd XMM0reg,xmm0
    20. movsd XMM1reg,xmm1
    21. movsd XMM2reg,xmm2
    22. movsd XMM3reg,xmm3
    23.  
    24. fxch  st(1)
    25. fmul  st(0),st(1)
    26. mulsd xmm0,xmm1
    27.  
    28. movsd XMM0reg,xmm0
    29. movsd XMM1reg,xmm1
    30. movsd XMM2reg,xmm2
    31. movsd XMM3reg,xmm3
    32.  
    33. fadd  qword ptr [ebp+$18]
    34. addsd xmm0,qword ptr [ebp+$18]
    35.  
    36. movsd XMM0reg,xmm0
    37. movsd XMM1reg,xmm1
    38. movsd XMM2reg,xmm2
    39. movsd XMM3reg,xmm3
    40.  
    41. fmul  st(0),st(1)
    42. mulsd xmm0,xmm1
    43.  
    44. movsd XMM0reg,xmm0
    45. movsd XMM1reg,xmm1
    46. movsd XMM2reg,xmm2
    47. movsd XMM3reg,xmm3
    48.  
    49. fadd  qword ptr [ebp+$10]
    50. addsd xmm0,qword ptr [ebp+$10]
    51.  
    52. movsd XMM0reg,xmm0
    53. movsd XMM1reg,xmm1
    54. movsd XMM2reg,xmm2
    55. movsd XMM3reg,xmm3
    56.  
    57. fmulp st(1),st(0)
    58. mulsd xmm0,xmm1
    59.  
    60. movsd XMM0reg,xmm0
    61. movsd XMM1reg,xmm1
    62. movsd XMM2reg,xmm2
    63. movsd XMM3reg,xmm3
    64.  
    65. fadd  qword ptr [ebp+$08]
    66. addsd xmm0,qword ptr [ebp+$08]
    67.  
    68. movsd XMM0reg,xmm0
    69. movsd XMM1reg,xmm1
    70. movsd XMM2reg,xmm2
    71. movsd XMM3reg,xmm3
    72.  
    73. movsd [esp-8],xmm0
    74. fld  qword ptr [esp-8]
    75.  
    76. movsd XMM0reg,xmm0
    77. movsd XMM1reg,xmm1
    78. movsd XMM2reg,xmm2
    79. movsd XMM3reg,xmm3
    80.  
    81. wait
    82. end;
     
  3. Mikl___

    Mikl___ Супермодератор Команда форума

    Публикаций:
    14
    Регистрация:
    25 июн 2008
    Сообщения:
    3.792
    Код не использует регистры XMM4-XMM7, и поэтому не было нужды создавать их просмотр. Код просмотра XMM располагается после каждых двух строк SSE2 кода. Все строки, кроме двух последних – это код с плавающей запятой, и SSE2 код, добавлен так, что бы каждая операция выполнялась как операция с плавающей запятой, так и как SSE2. данный путь делает возможным трассировать код и проверять, что делает SSE2 версия, сравнительно классической версии. Откройте окно FPU view, и смотрите, как изменяется стек FP, и одновременно как изменяются регистры XMM. Я разработал SSE2 код, просто добавляя SSE2 инструкции сразу после каждой строки FP кода.
    Код (ASM):
    1. fld  qword ptr [ebp+20h]
    2. movsd xmm0,qword ptr [ebp+20h]
    MOVSD копирует одну переменную двойной точности, из памяти по адресу [EBP+$20], в регистр XMM. “qword ptr” не требуется, но я сохранил это, что бы снять различие между SSE2 и FP кодом.
    Наибольшая разница между FP кодом и скалярным SSE2 кодом, состоит в том, что регистры FP организованы в виде стека, а регистры SSE2 нет. В первое время, при кодировании SSE2 кода, я просто игнорировал это, и затем после того, как я сделал все необходимые SSE2 строки, я вернулся назад, прошелся по всем строкам, строка за строкой и откорректировал их так, что бы они работали с корректным парой переменная/регистр. Активируя функции, определенными значениями, следуя двум следующим видам (например: X=2, A=3, B=4, C=5, D=6), и мы увидим, что сначала загружается “2”, затем “3”, затем 2 умножается на “3” и “2” переписывается “6” и так далее.
    Скалярным SSE2 соответствием для FMUL является MULSD. Суффикс SD означает Scalar – Double (Скаляр – Двойная точность).
    Код (ASM):
    1. fxch  st(1)
    2. fmul  st(0),st(1)
    3. mulsd xmm0,xmm1
    Скалярным SSE2 соответствием для FADD является ADDSD.
    Код (ASM):
    1. fadd  qword ptr [ebp+18h]
    2. addsd xmm0,qword ptr [ebp+18h]
    Продолжаем таким же образом, строка за строкой.
    FP код оставляет результат в ST(0), а SSE2 код оставляет результат в регистре XMM. Затем результат копируется из регистра XMM в ST(0) через ячейку памяти на стек.
    Код (ASM):
    1. movsd [esp-8],xmm0
    2. fld  qword ptr [esp-8]
    Эти две строки выполняют именно это. В ESP-8, восемь байт находятся выше верхушки стека, есть также еще несколько мест, которые мы могли бы использовать, как временное место для хранения результата. Первая строка копирует XMM0 во временное место, и затем последняя строка загружает его в стек FP. Эти две строки дают перегрузку, что делает маленькие SSE2 функции менее эффективными, чем их FP аналоги.
    После двойной проверки SSE2 кода, мы можем удалить инструментальный код, так же как и старый FP, оставив только скалярную SSE2 функцию с действительно необходимым кодом, без лишней перегрузки.
    Код (Pascal):
    1. function ArcSinApprox3j(X, A, B, C, D : Double) : Double;
    2. asm
    3. //Result := ((A*X + B)*X + C)*X + D;
    4. movsd xmm0,qword ptr [ebp+$20]
    5. movsd xmm1,qword ptr [ebp+$28]
    6. mulsd xmm0,xmm1
    7. addsd xmm0,qword ptr [ebp+$18]
    8. mulsd xmm0,xmm1
    9. addsd xmm0,qword ptr [ebp+$10]
    10. mulsd xmm0,xmm1
    11. addsd xmm0,qword ptr [ebp+$08]
    12. movsd [esp-8],xmm0
    13. fld  qword ptr [esp-8]
    14. end;
    Теперь это станет более красивым, после удаления не нужного подчеркивания “qword ptr”.
    Код (Pascal):
    1. function ArcSinApprox3j(X, A, B, C, D : Double) : Double;
    2. asm
    3. //Result := ((A*X + B)*X + C)*X + D;
    4. movsd xmm0, [ebp+$20]
    5. movsd xmm1, [ebp+$28]
    6. mulsd xmm0,xmm1
    7. addsd xmm0, [ebp+$18]
    8. mulsd xmm0,xmm1
    9. addsd xmm0, [ebp+$10]
    10. mulsd xmm0,xmm1
    11. addsd xmm0, [ebp+8]
    12. movsd [esp-8],xmm0
    13. fld  qword ptr [esp-8]
    14. end;
    Заменим указатели на имена параметров
    Код (Pascal):
    1. function ArcSinApprox3j(X, A, B, C, D : Double) : Double;
    2. asm
    3. //Result := ((A*X + B)*X + C)*X + D;
    4. movsd xmm0, A
    5. movsd xmm1, X
    6. mulsd xmm0,xmm1
    7. addsd xmm0, B
    8. mulsd xmm0,xmm1
    9. addsd xmm0, C
    10. mulsd xmm0,xmm1
    11. addsd xmm0, D
    12. movsd [esp-8],xmm0
    13. fld  qword ptr [esp-8]
    14. end;
    И наконец, проверим, как работает данная версия?
    Результат равен 45882 пунктам.
    Данная версия немного медленнее, чем версия с плавающей запятой, которая получила 48292 пункта. Мы должны разобраться, в чем причина этого. Толи причина в перегрузки в двух последних строках, то ли в 2-тактном throughput инструкций ADDSD и MULSD? Перегрузка может быть удалена, путем передачи параметра как выходного (OUT параметр) или мы должны встроить (inline) в функцию. Было бы очень интересно для нас насколько велико преимущество от встраивания такой относительно маленькой функции. Во первых, мы избавляемся от передачи пяти параметров с двойной точностью, каждый из которых занимает восемь байт. Посмотрим насколько много кода используется для этого.
    Код (ASM):
    1. push dword ptr [ebp+$14]
    2. push dword ptr [ebp+$10]
    3. push dword ptr [ebp+$34]
    4. push dword ptr [ebp+$30]
    5. push dword ptr [ebp+$2c]
    6. push dword ptr [ebp+$28]
    7. push dword ptr [ebp+$24]
    8. push dword ptr [ebp+$20]
    9. push dword ptr [ebp+$1c]
    10. push dword ptr [ebp+$18]
    11. call dword ptr [ArcSinApproxFunction]
    12. fstp qword ptr [ebp+8]
    Не менее десяти инструкций PUSH, каждая помещает в стек только четыре байта, половина от каждого параметра. Заметим, что регистровое соглашение о вызове, смотрит серьезно на их имена и передает параметры вместо использования FP стека. Затем мы должны иметь пять инструкций FLD, которые могли бы устранить ненужность загрузки параметров со стека в функцию. Это значит, что пять FLD инструкций в функции могли бы быть заменены пятью инструкциями FLD, в точке вызова и десять PUSH инструкции ушли бы в небытие. Это могло бы драматическим образом увеличить быстродействие. Встраивание функции вместо вызова, так же уменьшило перегрузку, за счет отсутствия пары инструкций CALL/RET, которая конечно меньше, чем перегрузка от такого количества PUSH, и это дало нам следующую производительность, на преобразованной в register2 соглашении об вызове :).
    Inlined ArcSinApprox3i 156006
    Inlined ArcSinApprox3j 160000
    Улучшение составляет 400%.
    Я подлинно желаю Борланду ввести истинное соглашение по вызову для параметров с плавающей запятой в самом ближайшем будущем.
    SSE2 версия только на 3% быстрее, чем IA32 версия. Но это больше относится к должной реализации SSE2.
     
    rococo795 нравится это.