GCC против Clang: включение функции с -fPIC

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

// foo.cxx
int last;

int next() {
return ++last;
}

int index(int scale) {
return next() << scale;
}

При компиляции с gcc 7.2:

$ g++ -std=c++11 -O3 -fPIC

Это испускает:

next():
movq    last@GOTPCREL(%rip), %rdx
movl    (%rdx), %eax
addl    $1, %eax
movl    %eax, (%rdx)
ret
index(int):
pushq   %rbx
movl    %edi, %ebx
call    next()@PLT    ## next() not inlined, call through PLT
movl    %ebx, %ecx
sall    %cl, %eax
popq    %rbx
ret

Однако при компиляции того же кода с теми же флагами вместо этого используется clang 3.9:

next():                               # @next()
movq    last@GOTPCREL(%rip), %rcx
movl    (%rcx), %eax
incl    %eax
movl    %eax, (%rcx)
retq

index(int):                              # @index(int)
movq    last@GOTPCREL(%rip), %rcx
movl    (%rcx), %eax
incl    %eax              ## next() was inlined!
movl    %eax, (%rcx)
movl    %edi, %ecx
shll    %cl, %eax
retq

GCC звонки next() через PLT, Clang встраивает это. Оба все еще ищут last из полученного. Для компиляции в Linux, правильно ли clang сделать эту оптимизацию, а gcc упускает простое встраивание, или неверно clang, чтобы сделать эту оптимизацию, или это просто проблема QoI?

8

Решение

Я не думаю, что в стандарте так много деталей. Это просто говорит о том, что примерно, если символ имеет внешнюю связь в разных единицах перевода, это один и тот же символ. Это делает верную версию Clang.

С этого момента, насколько мне известно, мы вышли за рамки стандарта. Выбор компиляторов отличается от того, что они считают полезным -fPIC выход.

Обратите внимание, что g++ -c -std=c++11 -O3 -fPIE выходы:

0000000000000000 <_Z4nextv>:
0:   8b 05 00 00 00 00       mov    0x0(%rip),%eax        # 6 <_Z4nextv+0x6>
6:   83 c0 01                add    $0x1,%eax
9:   89 05 00 00 00 00       mov    %eax,0x0(%rip)        # f <_Z4nextv+0xf>
f:   c3                      retq

0000000000000010 <_Z5indexi>:
10:   8b 05 00 00 00 00       mov    0x0(%rip),%eax        # 16 <_Z5indexi+0x6>
16:   89 f9                   mov    %edi,%ecx
18:   83 c0 01                add    $0x1,%eax
1b:   89 05 00 00 00 00       mov    %eax,0x0(%rip)        # 21 <_Z5indexi+0x11>
21:   d3 e0                   shl    %cl,%eax
23:   c3                      retq

Так что GCC делает знать, как это оптимизировать. Он просто выбирает не при использовании -fPIC, Но почему? Я вижу только одно объяснение: сделать возможным переопределить символ во время динамического связывания и увидеть эффекты последовательно. Техника известна как расположение символов.

В общей библиотеке, если index звонки next, как next является глобально видимым, gcc должен учитывать возможность того, что next может быть вставлен. Так что он использует PLT. Когда используешь -fPIE однако вам не разрешено вставлять символы, поэтому gcc включает оптимизацию.

Так что, лязг не так? Нет. Но gcc, кажется, обеспечивает лучшую поддержку для вставки символов, что удобно для инструментов кода. Это делается за счет некоторых накладных расходов, если кто-то использует -fPIC вместо -fPIE хотя для создания его исполняемого файла.


Дополнительные примечания:

В эта запись в блоге от одного из разработчиков gcc, он упоминает, примерно в конце поста:

Сравнивая некоторые тесты с clang, я заметил, что clang фактически игнорирует правила вставки ELF. Пока это баг, я решил добавить -fno-semantic-interposition флаг GCC, чтобы получить подобное поведение. Если вставка нежелательна, официальный ответ ELF — использовать скрытую видимость, а если символ необходимо экспортировать, определите псевдоним. Это не всегда практично делать руками.

Следуя этому примеру, я оказался на x86-64 ABI spec. В разделе 3.5.5 он требует, чтобы все функции, вызывающие глобально видимые символы, проходили через PLT (до определения точной последовательности инструкций, используемой в зависимости от модели памяти).

Поэтому, хотя это и не нарушает стандарт C ++, игнорирование семантической вставки, по-видимому, нарушает ABI.


Последнее слово: не знал, где это поставить, но это может вас заинтересовать. Я избавлю вас от дампов, но мои тесты с опциями objdump и компилятора показали, что:

На стороне gcc вещей:

  • gcc -fPIC: доступ к last проходит через GOT, звонки на next() проходит через PLT.
  • gcc -fPIC -fno-semantic-interposition: last проходит через, next() указывается
  • gcc -fPIE: last является IP-относительным, next() указывается
  • -fPIE подразумевает -fno-semantic-interposition

На лягушке:

  • clang -fPIC: last проходит через, next() указывается
  • clang -fPIE: last проходит через, next() указывается

И модифицированная версия, которая компилируется в IP-относительную, встроена в оба компилятора:

// foo.cxx
int last_ __attribute__((visibility("hidden")));
extern int last __attribute__((alias("last_")));

int __attribute__((visibility("hidden"))) next_()
{
return ++last_;
}
// This one is ugly, because alias needs the mangled name. Could extern "C" next_ instead.
extern int next() __attribute__((alias("_Z5next_v")));

int index(int scale) {
return next_() << scale;
}

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

13

Другие решения

Других решений пока нет …

По вопросам рекламы [email protected]