За movzbl следует testl быстрее, чем за testb? - PullRequest
3 голосов
/ 19 июня 2020

Рассмотрим код C:

int f(void) {
    int ret;
    char carry;

    __asm__(
        "nop # do something that sets eax and CF"
        : "=a"(ret), "=@ccc"(carry)
    );

    return carry ? -ret : ret;
}

Когда я компилирую его с помощью gcc -O3, я получаю следующее:

f:
        nop # do something that sets eax and CF
        setc    %cl
        movl    %eax, %edx
        negl    %edx
        testb   %cl, %cl
        cmovne  %edx, %eax
        ret

Если я изменю char carry на int carry, Вместо этого я получаю следующее:

f:
        nop # do something that sets eax and CF
        setc    %cl
        movl    %eax, %edx
        movzbl  %cl, %ecx
        negl    %edx
        testl   %ecx, %ecx
        cmovne  %edx, %eax
        ret

Это изменение заменило testb %cl, %cl на movzbl %cl, %ecx и testl %ecx, %ecx. Программа фактически эквивалентна, и G CC знает об этом. В качестве доказательства этого, если я компилирую с -Os вместо -O3, то оба char carry и int carry приведут к одной и той же сборке:

f:
        nop # do something that sets eax and CF
        jnc     .L1
        negl    %eax
.L1:
        ret

Это похоже на одно из двух должно быть правдой, но я не уверен, что:

  1. A testb быстрее, чем movzbl, за которым следует testl, поэтому G CC использует последнее с int - это пропущенная оптимизация.
  2. A testb медленнее, чем movzbl, за которым следует testl, поэтому использование первого G CC с char является пропущенная оптимизация.

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

Между прочим, обычный рекомендуемый подход xor обнулить регистр перед setc не работает в моем реальном примере. Вы не можете сделать это после запуска встроенной сборки, поскольку xor перезапишет флаг переноса, и вы не можете сделать это до запуска встроенной сборки, поскольку в реальный контекст этого кода , каждый регистр общего назначения, закрывающий вызовы, каким-то образом уже используется.

1 Ответ

3 голосов
/ 19 июня 2020

Нет никаких недостатков, о которых я знаю, для чтения байтового регистра с test по сравнению с movzb.

Если вы собираетесь расширять с нуля, это также пропущенная оптимизация, чтобы не xor- обнуление reg перед оператором asm и setc в него, чтобы стоимость нулевого расширения была вне критического пути. (На процессорах, отличных от Intel IvyBridge +, где movzx r32, r8 не является нулевой задержкой). Если, конечно, есть бесплатный регистр. Недавний G CC иногда обнаруживает эту оптимизацию нуля / set-flags / set cc для генерации 32-битного логического значения из инструкции установки флага, но часто пропускает его, когда все становится сложным.

К счастью для вас ваш реальный вариант использования в любом случае не может обеспечить эту оптимизацию (за исключением обнуления mov $0, %eax, что не соответствует критическому пути для задержки, но вызывает остановку частичного регистра в семействе Intel P6 и требует большего размера кода. ) Но это все еще пропущенная оптимизация для вашего тестового примера.

...