27.09.2020

Оптимизирую Scalar SSE

Первый вариант моей оптимизации путем развертывания цикла оказался не очень удачным, как совершенно справедливо указал Иван Колесников в комментариях к посту Scalar SSE. Так что исправляю.

1. Да, организация цикла были весьма корява, исправление существенно увеличило производительность.

2. Ручное переименование регистров, по всей видимости, сбивало с толку механизм, встроенный в процессор. Отказ от него также привел к увеличению производительности.

3. Ручное перемешивание кода, опять же, не давало нормально работать механизму внеочередного исполнения команд. После того, как поставил команды в естественном порядке, код стал работать быстрее.

4. А вот предложение заменить вычитание умножением на -1 и сложением наоборот, замедляло производительность.

5. Не стал реализовать концовку аналогом оператора case (switch). Вот мои соображения:
во-первых, такой оператор не очень быстр: тут либо несколько условных переходов, либо, более быстро косвенный безусловный переход по адресу в памяти (8-ми байтному); не уверен, что адрес будет в кэше, предыдущий длинный цикл по строке скорее всего его вытеснит; и переход такой будет лишь один раз на все итерации внутреннего цикла;
во-вторых, такой вариант не очень хорошо ложится на последующий переход к векторному варианту решения.
Спорно, конечно, но решил что этот вариант не сильно ускорит решение СЛАУ.

Насколько удалось ускорить, покажут будущие тесты.

Вскоре возьмусь за векторный вариант, для начала на SSE.

Ну и напоследок код главного цикла (цикл для верхней строки тоже аналогично оптимизировал; ну и то же самое сделал для FPU):

procedure MakeDoubleNextRowsPASM;
asm
// R8 - P, R9w - n, R10w - index
  lea EBX, [R10d+1]; // BX - i
  cmp BX, R9w;
  jae @exit;
@Loop1:
  mov RCX, [R8 + RBX*8]; // RCX - S
  lea EDX, [R10d+1];
  mov EAX, R9d;  // --
  movsd XMM0, [RCX+R10*8];
  sub EAX, 3;
  cmp EDX, EAX; // --
  jg @Loop2_2;
@Loop2:
  movapd XMM1, XMM0;
  mulsd XMM1, [R11+RDX*8];
  movsd XMM2, [RCX+RDX*8];
  subsd XMM2, XMM1;
  movsd [RCX+RDX*8], XMM2;
  movapd XMM1, XMM0;
  mulsd XMM1, [R11+RDX*8+8];
  movsd XMM2, [RCX+RDX*8+8];
  subsd XMM2, XMM1;
  movsd [RCX+RDX*8+8], XMM2;
  movapd XMM1, XMM0;
  mulsd XMM1, [R11+RDX*8+16];
  movsd XMM2, [RCX+RDX*8+16];
  subsd XMM2, XMM1;
  movsd [RCX+RDX*8+16], XMM2;
  movapd XMM1, XMM0;
  mulsd XMM1, [R11+RDX*8+24];
  movsd XMM2, [RCX+RDX*8+24];
  subsd XMM2, XMM1;
  movsd [RCX+RDX*8+24], XMM2;
  add DX, 4;
  cmp EDX, EAX; // --
  jle @Loop2;
@Loop2_2:
  add EAX, 2;
  cmp EDX, EAX;
  jg @Loop2_1;
  movapd XMM1, XMM0;
  mulsd XMM1, [R11+RDX*8];
  movsd XMM2, [RCX+RDX*8];
  subsd XMM2, XMM1;
  movsd [RCX+RDX*8], XMM2;
  movapd XMM1, XMM0;
  mulsd XMM1, [R11+RDX*8+8];
  movsd XMM2, [RCX+RDX*8+8];
  subsd XMM2, XMM1;
  movsd [RCX+RDX*8+8], XMM2;
  add DX, 2;
@loop2_1:
  cmp DX, R9w; //--
  ja @Loop2_Fin;
  movapd XMM1, XMM0;
  mulsd XMM1, [R11+RDX*8];
  movsd XMM2, [RCX+RDX*8];
  subsd XMM2, XMM1;
  movsd [RCX+RDX*8], XMM2;
@Loop2_Fin:
  inc BX;
  cmp BX, R9w;
  jb @Loop1;
@exit:
end;

3 комментария:

  1. Отлично, хорошо когда идеи полезны!

    > Ручное переименование регистров ... Ручное перемешивание кода

    Раз код теперь повторяющийся, я бы посмотрел в сторону макросов в ассемблере для описания одной итерации внутреннего цикла: ему на вход константа смещения, а он генерирует код для итерации, должно быть намного проще читать, да и изменять тоже.

    > А вот предложение заменить вычитание умножением на -1 и сложением наоборот, замедляло производительность

    Я как-то не ожидал что будет замедление, минимум не должно ухудшить. Может мы друг друга не поняли? Я имел ввиду умножить xmm0 один ра ДО внутреннего цикла на -1, а в цикле movsd & subsd заменить на addsd. Вы случайно не внутри цикла добавили умножение? Тогда это бы объяснило замедление.

    > Не стал реализовать концовку аналогом оператора case (switch)

    В целом вроде стандартная техника, но согласен, я бы тоже наверное не стал реализовывать: код сложнее, а результат если и будет то думаю доли процента.

    А не пробовали по 8 разворачивать? Регистров вроде хватает, опять же cache line целиком задействуется, что вроде правильнее. Макросы упросят тестирование данных предположений.

    Еще прочитал что правильно делать пре-цикл для выравнивания основного развернутого цикла по границам cache line, но это очередное усложнение...

    ОтветитьУдалить
  2. > Раз код теперь повторяющийся, я бы посмотрел в сторону макросов в ассемблере...
    Насколько я понял, в Delphi вроде так нельзя делать. Хотя надо проверить... И почему-то я их не люблю, макросы. )

    > Может мы друг друга не поняли? Я имел ввиду умножить xmm0 один ра ДО внутреннего цикла на -1,
    И точно, я в очередной раз протупил. Можно ведь умножить до цикла! Тогда, возможно, будет и быстрее.

    > А не пробовали по 8 разворачивать?
    Нет, не пробовал. Возможно, потестю ради интереса.

    > Еще прочитал что правильно делать пре-цикл...
    Да, тоже про это слышал, обычно вроде реализуют смещение начала цикла с выравниванием, и безусловный переход на него. А прециклом, это как?

    ОтветитьУдалить
    Ответы
    1. > А прециклом, это как?
      Похоже мы об одном говорим, просто я новое слово изобрел. В общем я про короткий цикл перед основным (для double от 0 до 7 итераций) который выровнет основной по 64-бит границе, но это конечно при условии что все массивы изначально выровнены по 64-байтам, но сейчас думаю про это - как-то ну очень муторно, ведь хвост то тоже нужно будет добивать и того 2 коротких цикла: перед основным и после. Сложно представить что это будет быстрее и так сходу не понятно с какой кстати. Да и найти не могу где читал про это, может мне вообще приснилось? Я бы не стал с этим возиться.

      Удалить