Подтвердить что ты не робот

Gcc -O0 все еще оптимизирует "неиспользуемый" код. Есть ли флаг компиляции, чтобы изменить это?

Как я поднял в этот вопрос, gcc удаляет (да, с -O0) строку кода _mm_div_ss(s1, s2); предположительно, потому что результат не сохраняется, Однако это должно вызвать исключение с плавающей запятой и повысить SIGFPE, что не может произойти, если вызов удален.

Вопрос: Есть ли флаг или несколько флагов для передачи в gcc, чтобы код был скомпилирован как-есть? Я думаю что-то вроде fno-remove-unused, но я не вижу ничего подобного. В идеале это был бы флаг компилятора вместо того, чтобы менять исходный код, но если это не поддерживается, есть ли какой-нибудь gcc-атрибут/прагма для использования?

Вещи, которые я пробовал:

$ gcc --help=optimizers | grep -i remove

нет результатов.

$ gcc --help=optimizers | grep -i unused

нет результатов.

И явно отключив все флаги мертвого кода/исключения - обратите внимание, что нет предупреждения о неиспользуемом коде:

$ gcc -O0 -msse2 -Wall -Wextra -pedantic -Winline \
     -fno-dce -fno-dse -fno-tree-dce \
     -fno-tree-dse -fno-tree-fre -fno-compare-elim -fno-gcse  \
     -fno-gcse-after-reload -fno-gcse-las -fno-rerun-cse-after-loop \
     -fno-tree-builtin-call-dce -fno-tree-cselim a.c
a.c: In function ‘main’:
a.c:25:5: warning: ISO C90 forbids mixed declarations and code [-Wpedantic]
     __m128 s1, s2;
     ^
$

Исходная программа

#include <stdio.h>
#include <signal.h>
#include <string.h>
#include <xmmintrin.h>

static void sigaction_sfpe(int signal, siginfo_t *si, void *arg)
{
    printf("%d,%d,%d\n", signal, si!=NULL?1:0, arg!=NULL?1:0);
    printf("inside SIGFPE handler\nexit now.\n");
    exit(1);
}

int main()
{
    struct sigaction sa;

    memset(&sa, 0, sizeof(sa));
    sigemptyset(&sa.sa_mask);
    sa.sa_sigaction = sigaction_sfpe;
    sa.sa_flags = SA_SIGINFO;
    sigaction(SIGFPE, &sa, NULL);

    _mm_setcsr(0x00001D80);

    __m128 s1, s2;
    s1 = _mm_set_ps(1.0, 1.0, 1.0, 1.0);
    s2 = _mm_set_ps(0.0, 0.0, 0.0, 0.0);
    _mm_div_ss(s1, s2);

    printf("done (no error).\n");

    return 0;
}

Компиляция вышеуказанной программы дает

$ ./a.out
done (no error).

Изменение строки

_mm_div_ss(s1, s2);

to

s2 = _mm_div_ss(s1, s2); // add "s2 = "

дает ожидаемый результат:

$ ./a.out
inside SIGFPE handler

Изменить с более подробной информацией.

Это похоже на атрибут __always_inline__ в определении _mm_div_ss .

$ cat t.c
int
div(int b)
{
    return 1/b;
}

int main()
{
    div(0);
    return 0;
}


$ gcc -O0 -Wall -Wextra -pedantic -Winline t.c -o t.out
$  

(никаких предупреждений или ошибок)

$ ./t.out
Floating point exception
$

vs ниже (такое же, за исключением атрибутов функции)

$ cat t.c
__inline int __attribute__((__always_inline__))
div(int b)
{
    return 1/b;
}

int main()
{
    div(0);
    return 0;
}

$ gcc -O0 -Wall -Wextra -pedantic -Winline t.c -o t.out
$   

(никаких предупреждений или ошибок)

$ ./t.out
$

Добавление атрибута функции __warn_unused_result__ по крайней мере дает полезное сообщение:

$ gcc -O0 -Wall -Wextra -pedantic -Winline t.c -o t.out
t.c: In function ‘main’:
t.c:9:5: warning: ignoring return value of ‘div’, declared with attribute warn_unused_result [-Wunused-result]
     div(0);
     ^

изменить:

Некоторые обсуждения gcc рассылки. В конечном счете, я думаю, что все работает по назначению.

4b9b3361

Ответ 1

GCC здесь не "оптимизирует". Он просто не создает бесполезный код. Кажется очень распространенной иллюзией, что существует некоторая чистая форма кода, которую должен генерировать компилятор, и любые изменения в этом являются "оптимизацией". Нет такой вещи.

Компилятор создает некоторую структуру данных, которая представляет собой код, а затем применяет некоторые преобразования в этой структуре данных и из нее генерирует ассемблер, который затем скомпилируется до инструкций. Если вы компилируете без "оптимизации", это просто означает, что компилятор будет делать только наименьшие усилия для генерации кода.

В этом случае весь оператор бесполезен, потому что он ничего не делает и сразу отбрасывается (после расширения встроенных строк и того, что встроенные значения означают, что он эквивалентен записи a/b;), разница заключается в том, что запись a/b; выдает предупреждение о statement with no effect, в то время как встроенные функции, вероятно, не обрабатываются теми же предупреждениями). Это не оптимизация, компилятор действительно должен будет затрачивать дополнительные усилия, чтобы придумать смысл для бессмысленного утверждения, а затем подделать временную переменную, чтобы сохранить результат этого утверждения, а затем выбросить его.

То, что вы ищете, - это не флаги для отключения оптимизации, а флаги пессимизации. Я не думаю, что разработчики компилятора тратят время на реализацию таких флагов. Кроме, может быть, в качестве шутки в апреле.

Ответ 2

Почему gcc не выбрасывает указанную инструкцию?

Компилятор создает код, который должен иметь наблюдаемое поведение, указанное стандартом. Все, что не наблюдается, может быть изменено (и оптимизировано) по желанию, так как оно не изменяет поведение программы (как указано).

Как вы можете победить его в подчинении?

Хитрость заключается в том, чтобы заставить компилятор считать, что поведение конкретного фрагмента кода действительно наблюдаемо.

Поскольку эта проблема часто встречается в микро-бенчмарке, я советую вам посмотреть, как (например) Google-Benchmark обращается к этому. Из benchmark_api.h получаем:

template <class Tp>
inline void DoNotOptimize(Tp const& value) {
    asm volatile("" : : "g"(value) : "memory");
}

Детали этого синтаксиса скучны, для нашей цели нам нужно только знать:

  • "g"(value) указывает, что value используется как ввод в оператор
  • "memory" - это барьер чтения/записи времени компиляции

Итак, мы можем изменить код на:

asm volatile("" : : : "memory");

__m128 result = _mm_div_ss(s1, s2);

asm volatile("" : : "g"(result) : );

Что:

  • заставляет компилятор учитывать, что s1 и s2 могут быть изменены между их инициализацией и использованием
  • заставляет компилятор учитывать, что используется результат операции

Нет необходимости в каком-либо флаге, и он должен работать на любом уровне оптимизации (я тестировал его на https://gcc.godbolt.org/ на -O3).

Ответ 3

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

Позвольте сократить ваш пример от специфических свойств компилятора до простого старого добавления:

int foo(int num) {
    num + 77;
    return num + 15;
}

Нет кода для + 77 сгенерированного:

foo(int):
        push    rbp
        mov     rbp, rsp
        mov     DWORD PTR [rbp-4], edi
        mov     eax, DWORD PTR [rbp-4]
        add     eax, 15
        pop     rbp
        ret

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

Но сохранение этого результата в (даже неиспользуемой) переменной заставляет компилятор генерировать код для добавления:

int foo(int num) {
  int baz = num + 77;
  return num + 15;
}

Сборка:

foo(int):
    push    rbp
    mov     rbp, rsp
    mov     DWORD PTR [rbp-20], edi
    mov     eax, DWORD PTR [rbp-20]
    add     eax, 77
    mov     DWORD PTR [rbp-4], eax
    mov     eax, DWORD PTR [rbp-20]
    add     eax, 15
    pop     rbp
    ret

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

Моя рекомендация должна быть явной о ваших намерениях и помещать результат выражения в volatile (и, следовательно, -removable от оптимизатора).

@Matthieu M указал, что недостаточно предотвратить предварительное вычисление значения. Поэтому для чего-то большего, чем игра с сигналами, вы должны использовать документированные способы выполнения точной инструкции, которую вы хотите (возможно, volatile встроенная сборка).