Требуется ли знак или нуль при добавлении 32-битного смещения к указателю для ABI x86-64?

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

void Test(int *out, int offset)
{
    out[offset] = 1;
}
-------------------------------------
movslq  %esi, %rsi
movl    $1, (%rdi,%rsi,4)
ret

Сначала мне показалось, что моему компилятору было предложено добавить от 32 до 64-битных целых чисел, но я подтвердил это поведение с Intel ICC 11, ICC 14 и GCC 5.3.

Этот поток подтверждает мои выводы, но неясно, требуется ли знак или нулевое расширение. Это расширение знака/нуля было бы необходимо только в том случае, если верхние 32 бита еще не установлены. Но не будет ли x86-64 ABI достаточно умен, чтобы требовать этого?

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

Ответ 1

Да, вы должны предположить, что старшие 32 бита регистра arg или возвращаемого значения содержат мусор. С другой стороны, вам разрешено оставлять мусор на высоких 32 при звонке или возвращении себя. то есть бремя лежит на приемной стороне, чтобы игнорировать старшие биты, а не на проходящей стороне, чтобы чистить старшие биты.

Вам нужно подписать или обнулить до 64 бит, чтобы использовать значение в 64-битном эффективном адресе. В x32 ABI gcc часто использует 32-битные эффективные адреса вместо того, чтобы использовать 64-битный размер операнда для каждой инструкции, модифицирующей потенциально отрицательное целое число, используемое в качестве индекса массива.


Стандарт:

SysV ABI x86-64 говорит только о том, какие части регистра обнуляются для _Bool (он же bool). Страница 20:

Когда значение типа _Bool возвращается или передается в регистре или в стеке, бит 0 содержит значение истинности, а биты с 1 по 7 должны быть равны нулю (сноска 14: другие биты остаются неуказанными, поэтому потребительская сторона этих значений может полагайтесь на 0 или 1 при усечении до 8 бит)

Кроме того, материал о %al содержит количество аргументов регистра FP для функций varargs, а не весь %rax.

На странице github для документов ABI x32 и x86-64 имеется открытый вопрос об этом конкретном вопросе.

ABI не предъявляет никаких дополнительных требований или гарантий к содержанию старших частей целочисленных или векторных регистров, содержащих аргументы или возвращаемые значения, поэтому их нет. У меня есть подтверждение этого факта по электронной почте от Майкла Мэтца (одного из сопровождающих ABI): "Как правило, если ABI не говорит, что что-то указано, вы не можете полагаться на это".

Он также подтвердил, что, например, = 3.6 makes code that could slow down from denormals in high elements //+and/or raise FP exceptions that it shouldn!'t. //happens only *without* -ffast-math float sumf_ilp(float a, float b,float c,float d,float e,float f,float g,float h) { return ((a+b)+(c+d))+((e+f)+(g+h)); } ')),filterAsm:(commentOnly:!t,directives:!t,labels:!t),version:3 rel="nofollow noreferrer">clang> = 3.6 использование addps которые могут замедлять или вызывать дополнительные исключения FP с мусором в старших элементах, является ошибкой (которая напоминает мне, что я должен сообщить об этом). Он добавляет, что однажды это было проблемой с реализацией AMD математической функции glibc. Обычный C-код может оставить мусор в старших элементах векторных регистров при передаче скалярных double или float аргументов.


Фактическое поведение, которое (пока) не задокументировано в стандарте:

Узкие аргументы функции, даже _Bool/bool, являются знаковыми или расширяются от нуля до 32 бит. clang даже создает код, который зависит от этого поведения (по-видимому, с 2007 года). ICC17 этого не делает, поэтому ICC и clang не совместимы с ABI, даже для C. Не вызывайте функции, скомпилированные clang, из кода, скомпилированного ICC для x86-64 SysV ABI, если есть какие-либо из первых 6 целочисленных аргументов уже 32-битного.

Это не относится к возвращаемым значениям, только аргументы: gcc и clang предполагают, что возвращаемые значения, которые они получают, имеют только действительные данные вплоть до ширины типа. Например, gcc создаст функции, возвращающие char которые оставляют мусор в старших 24 битах %eax, например.

В недавней ветке дискуссионной группы ABI было предложение прояснить правила расширения 8- и 16-битных аргументов до 32-битных и, возможно, фактически изменить ABI, чтобы потребовать этого. Основные компиляторы (кроме ICC) уже делают это, но это будет изменение в контракте между вызывающими и вызываемыми.

Вот пример (проверьте это с другими компиляторами или настройте код в проводнике компилятора Godbolt, где я включил много простых примеров, которые демонстрируют только один фрагмент головоломки, а также этот, который демонстрирует много):

extern short fshort(short a);
extern unsigned fuint(unsigned int a);

extern unsigned short array_us[];
unsigned short lookupu(unsigned short a) {
  unsigned int a_int = a + 1234;
  a_int += fshort(a);                 // NOTE: not the same calls as the signed lookup
  return array_us[a + fuint(a_int)];
}

# clang-3.8 -O3  for x86-64.    arg in %rdi.  (Actually in %di, zero-extended to %edi by our caller)
lookupu(unsigned short):
    pushq   %rbx                      # save a call-preserved reg for out own use.  (Also aligns the stack for another call)
    movl    %edi, %ebx                # If we didn't assume our arg was already zero-extended, this would be a movzwl (aka movzx)
    movswl  %bx, %edi                 # sign-extend to call a function that takes signed short instead of unsigned short.
    callq   fshort(short)
    cwtl                              # Don't trust the upper bits of the return value.  (This is cdqe, Intel syntax.  eax = sign_extend(ax))
    leal    1234(%rbx,%rax), %edi     # this is the point where we'd get a wrong answer if our arg wasn't zero-extended.  gcc doesn't assume this, but clang does.
    callq   fuint(unsigned int)
    addl    %ebx, %eax                # zero-extends eax to 64bits
    movzwl  array_us(%rax,%rax), %eax # This zero-extension (instead of just writing ax) is *not* for correctness, just for performance: avoid partial-register slowdowns if the caller reads eax
    popq    %rbx
    retq

Примечание: movzwl array_us(,%rax,2) будет эквивалентным, но не меньшим. Если бы мы могли зависеть от того, что старшие биты %rax fuint() обнуляются в возвращаемом значении fuint(), компилятор мог бы использовать array_us(%rbx, %rax, 2) вместо использования add insn.


Последствия для производительности

Оставлять high32 undefined намеренно, и я думаю, что это хорошее дизайнерское решение.

Игнорирование старшего 32 свободно при выполнении 32-битных операций. 32-битная операция ноль расширяет свой результат до 64-битного бесплатно, поэтому вам понадобятся только дополнительные mov edx, edi или что-то еще, если вы могли бы использовать reg непосредственно в 64-битном режиме адресации или 64-битной операции.

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

Расширение нуля до 64-битной независимо от подписи было бы бесплатным для большинства вызывающих абонентов и могло бы стать хорошим выбором для дизайна ABI. Так как аргументы arg в любом случае засорены, вызывающему абоненту уже нужно сделать что-то дополнительное, если он хочет сохранить полное 64-битное значение в вызове, где он передает только 32 нижнего уровня. Таким образом, он обычно стоит только дополнительно, когда вам нужен 64-битный результат для чего-то перед вызовом, а затем передать усеченную версию функции. В x86-64 SysV вы можете сгенерировать свой результат в RDI и использовать его, а затем call foo который будет смотреть только на EDI.

Размеры 16-битных и 8-битных операндов часто приводят к ложным зависимостям (AMD, P4 или Silvermont, а затем к семейству SnB) или к частичным задержкам регистров (до SnB) или к небольшим замедлениям (Sandybridge), поэтому недокументированное поведение требование о том, чтобы типы 8 и 16b были расширены до 32b для передачи аргументов, имеет некоторый смысл. См. Почему GCC не использует частичные регистры? для получения более подробной информации об этих микроархитектурах.


Это, вероятно, не имеет большого значения для размера кода в реальном коде, поскольку крошечные функции являются/должны быть static inline, а insns для обработки аргументов - это небольшая часть больших функций. Межпроцедурная оптимизация может устранить издержки между вызовами, когда компилятор может видеть оба определения, даже без вставки. (ИДК, насколько хорошо это делают компиляторы на практике.)

Я не уверен, поможет ли изменение сигнатур функций для использования uintptr_t или uintptr_t общую производительность с 64-битными указателями. Я бы не стал беспокоиться о пространстве стека для скаляров. В большинстве функций компилятор помещает/выталкивает достаточно регистров, сохраняющих вызовы (например, %rbx и %rbp), чтобы поддерживать свои собственные переменные в регистрах. Небольшое дополнительное пространство для разливов 8B вместо 4B незначительно.

Что касается размера кода, работа с 64-битными значениями требует префикса REX для некоторых insns, которые в противном случае не понадобились бы. Расширение нуля до 64-битного происходит бесплатно, если для 32-битного значения требуются какие-либо операции, прежде чем оно будет использовано в качестве индекса массива. Расширение знака всегда требует дополнительной инструкции, если это необходимо. Но компиляторы могут подписывать-расширять и работать с ним как с 64-разрядным знаковым значением с самого начала, чтобы сохранить инструкции, за счет необходимости большего количества префиксов REX. (Переполнением со знаком является UB, оно не определено для переноса, поэтому компиляторы часто могут избежать повторного расширения знака внутри цикла с помощью int i, использующего arr[i].)

Современные процессоры, как правило, больше заботятся о количестве insn, чем о размере insn. Горячий код часто запускается из кэша UOP в процессорах, которые их имеют. Тем не менее, меньший код может улучшить плотность в кэше UOP. Если вы можете сохранить размер кода, не используя больше или медленнее insns, то это выигрыш, но, как правило, не стоит жертвовать чем-то другим, если только он не имеет большого размера кода.

Например, одна дополнительная инструкция LEA, позволяющая [reg + disp8] обращаться к дюжине последующих инструкций вместо disp32. Или xor eax,eax перед несколькими командами mov [rdi+n], 0 чтобы заменить imm32 = 0 источником регистра. (Особенно, если это позволяет микро-слияние, где это было бы невозможно с RIP-родственником + немедленным, потому что на самом деле важен счетчик числа операций переднего плана, а не счетчик команд.)

Ответ 2

Как отмечает EOF, компилятор не может предположить, что верхние 32 бита 64-разрядного регистра, используемого для передачи 32-битного аргумента, имеют какое-то конкретное значение. Это делает необходимым знак или нуль.

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

Если вы действительно обеспокоены своим размером памяти и вам не нужно большее 64-разрядное адресное пространство, вы можете посмотреть x32 ABI, который использует типы ILP32, но поддерживает полный набор 64-битных команд.