Вызывает ли функция с инструкциями перед меткой точки входа какие-либо проблемы (связывание)?

Это действительно вопрос компоновщика/объектного файла, но пометка сборкой, поскольку компиляторы никогда этого не делают. (Хотя, может, и могли!)

Рассмотрим эту функцию, где я хочу обработать один особый случай с блоком кода, который находится в той же строке I-кэша, что и точка входа функции. Чтобы не перепрыгивать через него в обычном быстром пути, безопасно ли (относительно компоновки / общих библиотек / других инструментов, о которых я не подумал) помещать код для этого перед глобальным символом функции?

Я знаю, что это глупо/излишне, см. ниже. В основном мне было просто интересно. Независимо от того, полезен ли этот метод для создания кода, который на практике работает быстрее, я думаю, что это интересный вопрос.

.globl __nextafter_pjc      // double __nextafter_pjc(double x, double y)
.p2align 6  // unrealistic 64B alignment, just for the sake of argument

// GNU as local labels have the form  .L...
.Lequal_or_unordered:
    jp  .Lunordered
    movaps  %xmm1, %xmm0    # ISO C11 requires returning y, not x.  (matters for  -0.0 == +0.0)
    ret

######### Function entry point / global symbol here #############    
// .p2align something // tuning for Sandybridge, maybe best to just leave this unaligned, since it's only 6B from the alignment boundary
nextafter_pjc:
    ucomisd %xmm1, %xmm0
    je  .Lequal_or_unordered

    xorps   %xmm3, %xmm3
    comisd  %xmm3, %xmm0    // x==+/0.0 can be a special case: the sign bit may change
    je  .Lx_zero

    movq    %xmm0, %rax
    ...  // some mostly-branchless bit-ninjutsu that I have no idea how I'd get gcc to emit from C

    ret

.Lx_zero:
  ...
  ret
.Lunordered:
  ...
  ret

(Кстати, я возился с asm для nextafter, потому что я любопытно, как это реализовано в glibc. rel="nofollow">текущая реализация компилируется в какой-то действительно неприятный код с кучей ответвлений. Например, проверка обоих входных данных на наличие NaN должна выполняться с помощью сравнения FP, потому что это очень быстро, особенно в не-NaN кейс.)


В выводе дизассемблирования инструкции перед меткой группируются после инструкций предыдущей функции. например

0000000000400ad0 <frame_dummy>:
                ...
  400af0:       5d                      pop    %rbp
  400af1:       e9 7a ff ff ff          jmpq   400a70 <register_tm_clones>
  400af6:       66 2e 0f 1f 84 00 00    nopw   %cs:0x0(%rax,%rax,1)
  400afd:       00 00 00 
  400b00:       7a 56                   jp     400b58 <__nextafter_pjc+0x52>
  400b02:       0f 28 c1                movaps %xmm1,%xmm0
  400b05:       c3                      retq   

0000000000400b06 <__nextafter_pjc>:
  400b06:       66 0f 2e c1             ucomisd %xmm1,%xmm0
  400b0a:       74 f4                   je     400b00 <frame_dummy+0x30>
  400b0c:       0f 57 db                xorps  %xmm3,%xmm3
  400b0f:       66 0f 2f c3             comisd %xmm3,%xmm0
  400b13:       74 4b                   je     400b60 <__nextafter_pjc+0x5a>
  400b15:       66 48 0f 7e c0          movq   %xmm0,%rax
                ...

Обратите внимание, что 4-я инструкция в основном теле, comisd, начинается с 400b0f (и не полностью содержится в первом блоке, выровненном по 16B, который содержит точку входа функции). Так что, возможно, это не совсем оптимально для извлечения инструкций и декодирования для быстрого пути без веток, чтобы делать это именно таким образом. Впрочем, это всего лишь пример.

Таким образом, это похоже работает даже в начале файла. Это сбивает с толку objdump и не идеально подходит для gdb (но это не большая проблема). Объектные файлы ELF в любом случае не записывают размеры символов, поэтому nm --print-size в любом случае ничего не делает. (И nm --size-sort --print-size, который пытается вычислить размеры символов, странным образом не включает мою функцию.)

Я мало знаю об объектных файлах Windows. Там ничего страшнее не бывает?

Я немного беспокоюсь о правильности здесь: пытается ли когда-нибудь скопировать отдельные функции из объектных файлов, перенеся байты из их символьного адреса в следующий символьный адрес? Обычные библиотечные архивы (ar для статических библиотек) и компоновщики копируют целые объектные файлы, верно? В противном случае они не могли быть уверены, что копируют все необходимые статические данные.


Эта функция, вероятно, вызывается нечасто, и мы хотим свести к минимуму загрязнение кеша (I$, uop-кэш, предикторы ветвлений). И если что, оптимизируйте случай без кэширования с помощью предсказателей холодных переходов.

Это, вероятно, глупо, потому что случай без кэширования может происходить нечасто. Однако, если таким образом оптимизировать многие функции, общий размер кэша уменьшится, и, возможно, все они поместятся в кэш.

Обратите внимание, что последние процессоры Intel вообще не выполняют предсказание статических ветвлений, поэтому нет причин отдавать предпочтение прямым ветвям для обычно неиспользуемых ветвей.

Вместо того, чтобы по умолчанию принимать для обратных ветвей / не принимать для прямых для «неизвестных» ветвей, которых нет в BHT, мое понимание Микроархический документ Агнера Фога (глава о предсказании ветвления) заключается в том, что они не проверяют, является ли ветвь «новой» или нет. Они просто используют любую запись, которая уже есть в BHT, не очищая ее. Однако для Nehalem это может быть не совсем так .


person Peter Cordes    schedule 17.02.2016    source источник
comment
Однако размер функции важен для разделяемых библиотек, и в gas есть директива .size, которую gcc действительно использует, а nm показывает. Я забыл, для чего это на самом деле используется. Или, может быть, для функций важен только символ .type, а для не-функций размер имеет значение.   -  person Jester    schedule 18.02.2016


Ответы (1)


Есть простой способ сделать так, чтобы это выглядело совершенно нормально: поставить неглобальную метку перед кодом. Это делает его похожим (или на самом деле будет) static вспомогательной функцией.

Неглобальные функции могут вызывать друг друга с любым соглашением о вызовах, которое они хотят. Компиляторы C могут даже создавать такой код с оптимизацией времени компоновки / всей программы или даже просто оптимизацией static функций в единице компиляции. Переходы (а не вызовы) к другой функции уже используются для оптимизации хвостового вызова.

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

Я не уверен, есть ли подводные камни при создании метаданных .size ELF. Кажется, я читал, что это важно для функций, которые будут связаны с общими библиотеками.

Следующее должно нормально работать с любым инструментом, работающим с объектными файлами:

.globl __nextafter_pjc      // double __nextafter_pjc(double x, double y)
.p2align 6  // unrealistic 64B alignment, just for the sake of argument


nextafter_helper:  # not a local label, but not .globl either
.Lequal_or_unordered:
    jp  .Lunordered
    movaps  %xmm1, %xmm0    # ISO C11 requires returning y, not x.  (matters for  -0.0 == +0.0)
    ret

######### Function entry point / global symbol here #############    
// .p2align something?
__nextafter_pjc:
    ucomisd %xmm1, %xmm0
    je  .Lequal_or_unordered

    ...    
    ret

Нам не нужны нужны обычная метка и "локальная" метка, но использование разных меток для разных целей означает, что требуется меньше модификаций при изменении порядка элементов. (например, вы можете поместить блок .Lequal_or_unordered в другое место, не переименовывая его обратно в .L и не изменяя все переходы, нацеленные на него.) nextafter_equal_or_unordered будет работать как одно имя.

person Peter Cordes    schedule 19.02.2016