GCC x86-64 Неоптимальный выход сборки, почему? - PullRequest
8 голосов
/ 19 сентября 2011

При просмотре вывода сборки следующего кода (без оптимизаций -O2 и -O3 дают очень похожие результаты):

int main(int argc, char **argv)
{
    volatile float f1 = 1.0f;
    volatile float f2 = 2.0f;

    if(f1 > f2)
    {
        puts("+");
    }
    else if(f1 < f2)
    {
        puts("-");
    }

    return 0;
}

GCC делает то, за чем мне трудно следовать:

.LC2:
    .string "+"
.LC3:
    .string "-"
    .text
.globl main
    .type   main, @function
main:
.LFB2:
    pushq   %rbp
.LCFI0:
    movq    %rsp, %rbp
.LCFI1:
    subq    $32, %rsp
.LCFI2:
    movl    %edi, -20(%rbp)
    movq    %rsi, -32(%rbp)
    movl    $0x3f800000, %eax
    movl    %eax, -4(%rbp)
    movl    $0x40000000, %eax
    movl    %eax, -8(%rbp)
    movss   -4(%rbp), %xmm1
    movss   -8(%rbp), %xmm0
    ucomiss %xmm0, %xmm1
    jbe .L9
.L7:
    movl    $.LC2, %edi
    call    puts
    jmp .L4
.L9:
    movss   -4(%rbp), %xmm1
    movss   -8(%rbp), %xmm0
    ucomiss %xmm1, %xmm0
    jbe .L4
.L8:
    movl    $.LC3, %edi
    call    puts
.L4:
    movl    $0, %eax
    leave
    ret

Почему GCC дважды перемещает значения с плавающей точкой в ​​xmm0 и xmm1, а также дважды запускает ucomiss?

Не будет ли быстрее сделать следующее?

.LC2:
    .string "+"
.LC3:
    .string "-"
    .text
.globl main
    .type   main, @function
main:
.LFB2:
    pushq   %rbp
.LCFI0:
    movq    %rsp, %rbp
.LCFI1:
    subq    $32, %rsp
.LCFI2:
    movl    %edi, -20(%rbp)
    movq    %rsi, -32(%rbp)
    movl    $0x3f800000, %eax
    movl    %eax, -4(%rbp)
    movl    $0x40000000, %eax
    movl    %eax, -8(%rbp)
    movss   -4(%rbp), %xmm1
    movss   -8(%rbp), %xmm0
    ucomiss %xmm0, %xmm1
    jb  .L8 # jump if less than
    je  .L4 # jump if equal
.L7:
    movl    $.LC2, %edi
    call    puts
    jmp .L4
.L8:
    movl    $.LC3, %edi
    call    puts
.L4:
    movl    $0, %eax
    leave
    ret

Я вовсе не настоящий программист на ассемблере, но мне показалось странным иметь дублирующиеся инструкции. Есть ли проблема с моей версией кода?


Обновление

Если вы удалите летучее вещество, которое было у меня изначально, и замените его на scanf (), вы получите те же результаты:

int main(int argc, char **argv)
{
    float f1;
    float f2;

    scanf("%f", &f1);
    scanf("%f", &f2);

    if(f1 > f2)
    {
        puts("+");
    }
    else if(f1 < f2)
    {
        puts("-");
    }

    return 0;
}

и соответствующий ассемблер:

.LCFI2:
    movl    %edi, -20(%rbp)
    movq    %rsi, -32(%rbp)
    leaq    -4(%rbp), %rsi
    movl    $.LC0, %edi
    movl    $0, %eax
    call    scanf
    leaq    -8(%rbp), %rsi
    movl    $.LC0, %edi
    movl    $0, %eax
    call    scanf
    movss   -4(%rbp), %xmm1
    movss   -8(%rbp), %xmm0
    ucomiss %xmm0, %xmm1
    jbe .L9
.L7:
    movl    $.LC1, %edi
    call    puts
    jmp .L4
.L9:
    movss   -4(%rbp), %xmm1
    movss   -8(%rbp), %xmm0
    ucomiss %xmm1, %xmm0
    jbe .L4
.L8:
    movl    $.LC2, %edi
    call    puts
.L4:
    movl    $0, %eax
    leave
    ret

Окончательное обновление

После рассмотрения некоторых последующих комментариев кажется, что Хан (который комментировал пост Джонатана Леффлера) прибил эту проблему. GCC не проводит оптимизацию не потому, что не может, а потому, что я этого не говорил Кажется, все сводится к правилам IEEE с плавающей запятой, и для обработки строгих условий GCC не может просто выполнить переход, если выше, или переход, если ниже, после первого UCOMISS, потому что он должен обрабатывать все специальные условия чисел с плавающей запятой. При использовании рекомендации хана оптимизатора -ffast-math (ни один из флагов -Ox не включает -ffast-math, поскольку это может сломать некоторые программы) GCC делает именно то, что я искал:

Следующая сборка была произведена с использованием GCC 4.3.2 "gcc -S -O3 -ffast-math test.c"

.LC0:
    .string "%f"
.LC1:
    .string "+"
.LC2:
    .string "-"
    .text
    .p2align 4,,15
.globl main
    .type   main, @function
main:
.LFB25:
    subq    $24, %rsp
.LCFI0:
    movl    $.LC0, %edi
    xorl    %eax, %eax
    leaq    20(%rsp), %rsi
    call    scanf
    leaq    16(%rsp), %rsi
    xorl    %eax, %eax
    movl    $.LC0, %edi
    call    scanf
    movss   20(%rsp), %xmm0
    comiss  16(%rsp), %xmm0
    ja  .L11
    jb  .L12
    xorl    %eax, %eax
    addq    $24, %rsp
    .p2align 4,,1
    .p2align 3
    ret
    .p2align 4,,10
    .p2align 3
.L12:
    movl    $.LC2, %edi
    call    puts
    xorl    %eax, %eax
    addq    $24, %rsp
    ret
    .p2align 4,,10
    .p2align 3
.L11:
    movl    $.LC1, %edi
    call    puts
    xorl    %eax, %eax
    addq    $24, %rsp
    ret

Обратите внимание, что две инструкции UCOMISS теперь заменены одним COMISS, за которым сразу следуют JA (переход вверху, если выше) и JB (переход внизу, если ниже). GCC может прибегнуть к этой оптимизации, если вы позволите ей использовать -ffast-math!

UCOMISS против COMISS (http://www.softeng.rl.ac.uk/st/archive/SoftEng/SESP/html/SoftwareTools/vtune/users_guide/mergedProjects/analyzer_ec/mergedProjects/reference_olh/mergedProjects/instructions/instruct32_hh/vc315.htm): "Инструкция UCOMISS отличается от инструкции COMISS тем, что она сообщает о недопустимом исключении с плавающей запятой SIMD, только если исходный операнд является SNaN. Инструкция COMISS сигнализирует о недопустимости, если исходный операнд либо QNaN или SNaN. "

Еще раз спасибо всем за полезное обсуждение.

Ответы [ 2 ]

4 голосов
/ 19 сентября 2011

Вот еще одна причина: Если вы внимательно посмотрите на это, это НЕ то же самое выражение.

Они не являются дополнением друг к другу. Следовательно, вам все равно нужно сделать два сравнения. volatile заставит значения быть перезагруженными.

РЕДАКТИРОВАТЬ: (см. Комментарии, я забыл, что вы можете сделать это с флагами)

Чтобы ответить на новый вопрос:

Объединение этих двух ucomiss - не совсем очевидная оптимизация с точки зрения компилятора.

Чтобы их объединить, компилятор должен:

  1. Признайте, что ucomiss %xmm0, %xmm1 - это то же самое, что и ucomiss %xmm1, %xmm0.
  2. Затем он должен выполнить общий этап удаления подвыражения, чтобы вытащить его.

Все это нужно сделать после компилятор делает выбор инструкций. И большинство проходов оптимизации выполняется до выбора инструкций.

Что меня больше всего беспокоит, так это то, что f1 и f2 не хранятся в регистрах после того, как вы избавились от volatiles. -O3 действительно дает вам это?

3 голосов
/ 19 сентября 2011

Определитель volatile означает, что значения f1 и f2 могут изменяться так, что компилятор не может их обнаружить / ожидать, поэтому он должен обращаться к памяти каждый раз, когда использует f1 или f2. Сгенерированный код делает это - так что это правильно.

Сравните и сравните с кодом, который вы получите, если удалите квалификаторы volatile из любой переменной или из обеих переменных. В конечном итоге вам может понадобиться прочитать значения f1 и f2 откуда-то, чтобы компилятор не вычислял выражения во время компиляции.


В обновленном коде вы получаете два разных заклинания для инструкции ucomiss, хотя предыдущие movss инструкции одинаковы:

    ucomiss %xmm0, %xmm1
    ucomiss %xmm1, %xmm0

Порядок операндов для инструкции ucomiss меняется на обратный для условия:

if (f1 > f2)
if (f1 < f2)

Я не уверен, что оптимизатор оптимизирует, где мог, но вопрос выходит за рамки моего уровня знаний.

...