Является ли последовательность команд вызова / возврата x86 образует зависимую цепочку?

Рассмотрим следующую сборку x86-64:

inner:
   ...
   ret

outer:
.top:
   call inner
   dec  rdi
   jnz  .top
   ret

Функция outer просто неоднократно делает call к функции inner (чье тело не показано - оно может быть пустым).

Делает серию call инструкции в outerи соответствующий ret инструкции внутри inner сформировать зависимую цепочку на практике (для оценки эффективности)?

Существует несколько способов, которыми эта цепочка может быть сформирована. Например, ret зависит от времени ожидания предыдущего call инструкция, а затем делает последующее call инструкция зависит от ret, образуя call -> ret -> call цепь? Или возможно ret является независимым, но call нет, образуя call -> call цепь? Если есть цепочка, это через память, регистр, механизм стека, предиктор адреса возврата1 или что?

Мотивация: Этот вопрос возник из серии комментариев к другому вопросу, в основном этот комментарий и более ранние.


1 Терминология здесь может быть несколько неясной: механизм стека обычно понимается как обрабатывающий rsp- изменение инструкций в единый доступ с соответствующим смещением, так что push rax; push rbx может быть преобразован в нечто подобное mov [t0], rax; mov [t0 - 8], rbx где t0 некоторый временный регистр, который захватил значение rsp в какой-то момент. Понятно также обрабатывать аналогичные преобразования для call а также ret инструкции, которые оба модифицируют стек аналогично push а также pop а также включая прямой, косвенный (соответственно) прыжок. Процессор также включает в себя механизм для прогнозирования обратного непрямого скачка, который частично определяется как "механизм стека", но здесь я разделяю его на "предиктор обратного адреса".

1 ответ

Решение

Нет, предсказание ветвления + умозрительное выполнение нарушают зависимость сохранения / перезагрузки.

RIP (спекулятивно) известен внешнему интерфейсу от предиктора обратного адреса. Следующий call Таким образом, инструкция может выдвинуть обратный адрес, не дожидаясь ret выполнить (и фактически загрузить и проверить правильность предсказанного адреса возврата по отношению к данным из стека).

Спекулятивные хранилища могут входить в буфер хранилища и перенаправляться в хранилище.

Конечно, есть цепочка зависимостей, она не переносится циклами. Внеочередное выполнение скрывает это, сохраняя много итераций в полете.

Доказательство: call Магазин ломает то, что в противном случае было бы цепочкой зависимостей, переносимых циклом.

align 64
global _start
_start:
    mov     ebp, 250000000   ; I had been unrolling by 4, should have changed this to 5000... before measuring, but forgot.

align 32
.mainloop:
    call  delay_retaddr
    call  delay_retaddr

    dec ebp
    jg .mainloop

    xor edi,edi
    mov eax,231   ; __NR_exit_group  from /usr/include/asm/unistd_64.h
    syscall       ; sys_exit_group(0)

    ;; Placing this function *before* _start, or increasing the alignment,
    ;; makes it somewhat slower!
align 32
delay_retaddr:
    add qword [rsp], 0
    add qword [rsp], 0    ; create latency for the ret addr
    ret

Собрать и связать с yasm -felf64 -Worphan-labels -gdwarf2 foo.asm && ld -o foo foo.o, производящий статический бинарный файл ELF.

Профилированный (на i7-6700k) с ocperf.py, я получаю 0,99 инструкций за такт ядра:

$ taskset -c 3 ocperf.py stat -etask-clock,context-switches,cpu-migrations,page-faults,cycles,branches,instructions,uops_issued.any,uops_executed.thread,dsb2mite_switches.penalty_cycles -r2 ./foo

 Performance counter stats for './foo' (2 runs):

        645.770390      task-clock (msec)         #    1.000 CPUs utilized            ( +-  0.05% )
                 1      context-switches          #    0.002 K/sec                    ( +-100.00% )
                 0      cpu-migrations            #    0.000 K/sec
                 2      page-faults               #    0.004 K/sec                    ( +- 20.00% )
     2,517,412,984      cycles                    #    3.898 GHz                      ( +-  0.09% )
     1,250,159,413      branches                  # 1935.919 M/sec                    ( +-  0.00% )
     2,500,838,090      instructions              #    0.99  insn per cycle           ( +-  0.00% )
     4,010,093,750      uops_issued_any           # 6209.783 M/sec                    ( +-  0.03% )
     7,010,150,784      uops_executed_thread      # 10855.485 M/sec                   ( +-  0.02% )
            62,838      dsb2mite_switches_penalty_cycles #    0.097 M/sec                    ( +- 30.92% )

       0.645899414 seconds time elapsed                                          ( +-  0.05% )

С вызываемой функцией перед _start и значения выравнивания 128 IPC может снизиться с 0,99 до 0,84, что очень странно. Количество переключателей dsb2mite по-прежнему низкое, поэтому в основном он все еще работает из кэша UOP, а не из устаревших декодеров. (Этот процессор Skylake имеет обновление микрокода, которое отключает буфер цикла, в случае, если это будет актуально при всех этих переходах.)

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


Изменение add [rsp], 0 инструкции к [rsp+16] создает переносимую циклом цепочку зависимостей в другом месте, которая не сохраняется call, Таким образом, циклические узкие места в этой задержке пересылки магазина работают примерно на половине скорости.

# With  add qword [rsp+16], 0

 Performance counter stats for './foo' (2 runs):

   1212.339007      task-clock (msec)         #    1.000 CPUs utilized            ( +-  0.04% )
             2      context-switches          #    0.002 K/sec                    ( +- 60.00% )
             0      cpu-migrations            #    0.000 K/sec                  
             2      page-faults               #    0.002 K/sec                  
 4,727,361,809      cycles                    #    3.899 GHz                      ( +-  0.02% )
 1,250,292,058      branches                  # 1031.306 M/sec                    ( +-  0.00% )
 2,501,537,152      instructions              #    0.53  insn per cycle           ( +-  0.00% )
 4,026,138,227      uops_issued_any           # 3320.967 M/sec                    ( +-  0.02% )
 7,026,457,222      uops_executed_thread      # 5795.786 M/sec                    ( +-  0.01% )
       230,287      dsb2mite_switches_penalty_cycles #    0.190 M/sec                    ( +- 68.23% )

   1.212612110 seconds time elapsed                                          ( +-  0.04% )

Обратите внимание, что я все еще использую RSP-относительный адрес, так что все еще есть синхронизация стека. Я мог бы сохранить оба случая одинаковыми и избежать их в обоих, используя адрес относительно другого регистра (например, rbp) обратиться к месту, где call / ret сохранить / перезагрузить обратный адрес.

Я не думаю, что переменная задержка переадресации магазина (хуже в простых случаях прямой перезагрузки сразу) достаточна, чтобы объяснить разницу. Добавление избыточного назначения ускоряет код при компиляции без оптимизации. Это в 2 раза ускоряет нарушение зависимости. (0,99 IPC против 0,53 IPC, с одинаковыми инструкциями, только в другом режиме адресации.)

Инструкции длиннее на 1 байт с disp8 в режиме адресации, и в более быстрой версии была странность внешнего интерфейса с выравниванием, но перемещение объектов, похоже, ничего не меняет с [rsp+16] версия.


Использование версии, которая создает стойло для пересылки магазина (с add dword [rsp], 0) делает цепочку депо слишком длинной, чтобы OoO exec легко скрывался. Я не играл с этим огромное количество.

Другие вопросы по тегам