Да, вы должны предположить, что старшие 32 бита регистра аргумента или возвращаемого значения содержат мусор. С другой стороны, вам разрешено оставлять мусор на высоте 32 при звонке или возвращении самостоятельно. то есть нагрузка на принимающую сторону игнорировать старшие биты, а не на передающей стороне, чтобы очистить старшие биты.
Вам необходимо подписать или расширить нулем до 64 бит, чтобы использовать значение в 64-битном эффективном адресе. В x32 ABI gcc часто использует 32-битные эффективные адреса вместо 64-битных операндов. size для каждой инструкции, изменяющей потенциально отрицательное целое число, используемое в качестве индекса массива.
Стандарт:
x86-64 SysV ABI говорит только о том, какие части регистра обнуляются для _Bool
(он же bool
). Стр. 20:
Когда значение типа _Bool
возвращается или передается в регистр или в стек, бит 0 содержит значение истинности, а биты с 1 по 7 должны быть нулевыми (сноска 14: другие биты не указаны, поэтому потребительская сторона этих значений может полагаться на 0 или 1 при усечении до 8 бит)
Кроме того, информация о %al
хранении количества аргументов регистра FP для функций varargs, а не всего %rax
.
Есть открытая проблема github по этому вопросу на страница github для документов ABI x32 и x86-64 a >.
ABI не предъявляет никаких дополнительных требований или гарантий к содержимому старших частей целочисленных или векторных регистров, содержащих аргументы или возвращаемые значения, поэтому их нет. У меня есть подтверждение этого факта по электронной почте от Майкла Матца (одного из сопровождающих ABI): «Обычно, если ABI не говорит, что что-то определено, вы не можете полагаться на это».
Он также подтвердил, что, например, clang> = 3.6 использование addps
, которое может замедлить или вызвать дополнительные исключения FP с мусор в высоких элементах - это ошибка (что напоминает мне, что я должен сообщить об этом). Он добавляет, что однажды это было проблемой при реализации AMD математической функции glibc. Нормальный код C может оставлять мусор в верхних элементах векторных регистров при передаче скалярных double
или float
аргументов.
Фактическое поведение, которое (пока) не задокументировано в стандарте:
Узкие аргументы функции, даже _9 _ / _ 10_, расширяются знаком или нулем до 32 бит. clang даже создает код, зависящий от этого поведения (очевидно, с 2007 года). ICC17 не работает, поэтому ICC и clang несовместимы с ABI, даже для C. Не вызывайте скомпилированные clang функции из кода, скомпилированного с помощью ICC, для x86-64 SysV ABI, если любой из первых 6 целочисленных аргументов уже, чем 32-битный.
Это не относится к возвращаемым значениям, только args: gcc и clang предполагают, что возвращаемые значения, которые они получают, имеют только допустимые данные до ширины типа. gcc создаст функции, возвращающие char
, которые, например, оставляют мусор в старших 24 битах %eax
.
недавняя ветка в группе обсуждения ABI было предложение прояснить правила расширения 8- и 16-битных аргументов до 32-битных и, возможно, фактически изменить ABI, чтобы это потребовало. Основные компиляторы (кроме ICC) уже делают это, но это будет изменение контракта между вызывающими и вызываемыми объектами.
Вот пример (проверьте это с помощью других компиляторов или измените код в Godbolt Compiler Explorer, где я включил много простых примеров, которые демонстрируют только одну часть головоломки, а также то, что демонстрирует многое):
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()
, компилятор мог бы использовать array_us(%rbx, %rax, 2)
вместо add
insn.
Последствия для производительности
Оставить high32 неопределенным намеренно, и я думаю, что это хорошее дизайнерское решение.
При выполнении 32-битных операций игнорирование высоких 32 можно бесплатно. 32-битная операция с нулевым значением расширяет свой результат до 64-битной бесплатно, поэтому вам понадобится только дополнительный mov edx, edi
или что-то в этом роде, если вы могли бы использовать регистр напрямую в 64-битном режиме адресации или в 64-битном режиме. битовая операция.
Некоторые функции не спасают никаких insns от того, что их аргументы уже расширены до 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, обрабатывающие arg, являются небольшой частью более крупных функций. Межпроцедурная оптимизация может устранить накладные расходы между вызовами, когда компилятор может видеть оба определения, даже без встраивания. (IDK, насколько хорошо компиляторы справляются с этим на практике.)
Я не уверен, поможет ли изменение сигнатур функций для использования uintptr_t
общую производительность с 64-битными указателями или снизит ее. Я бы не стал беспокоиться о пространстве стека для скаляров. В большинстве функций компилятор выталкивает / выталкивает достаточно регистров с сохранением вызовов (например, %rbx
и %rbp
), чтобы сохранить свои собственные переменные в регистрах. Крошечное дополнительное пространство для разливов 8B вместо 4B незначительно.
Что касается размера кода, для работы с 64-битными значениями требуется префикс REX для некоторых insns, которые в противном случае не потребовались бы. Расширение нуля до 64-битного значения происходит бесплатно, если требуются какие-либо операции с 32-битным значением, прежде чем оно будет использовано в качестве индекса массива. Sign-extension всегда требует дополнительных инструкций, если это необходимо. Но компиляторы могут подписывать-расширять и работать с ним как с 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-relative + немедленным, потому что на самом деле имеет значение количество операций на интерфейсе, а не количество инструкций.)
person
Peter Cordes
schedule
21.04.2016
int
- это знаковый тип. Используйтеunsigned
или лучшеsize_t
, если вам не нужно расширение знака. - person EOF   schedule 19.04.2016void foo(uint32_t); void bar(uint64_t x){foo(x);}
- person EOF   schedule 19.04.2016int64_t t = something; foo((int)t, arr[t])
. Вам нужно вычислитьt
в 64-битном регистре, потому что индексирование массива использует все 64-битные. Если вы вычислили его в%rdi
, он уже находится в нужном месте для вызоваfoo
, но имеет много мусора. Кстати, @EOF: у ABI, похоже, есть некоторые неписаные правила о расширении 8b и 16b до 32b. Я был удивлен, но вижу свой ответ. - person Peter Cordes   schedule 21.04.2016I thought in 64-bit mode, every instruction computes a 64bit
нет, размер параметра по умолчанию в x86_64 составляет 32 бита. Для каждой 64-битной операции или доступа к старшим регистрам требуется префикс REX, поэтому он будет длиннее и не будет использоваться без необходимости. - person phuclv   schedule 21.04.2016