Требуется ли расширение знака или нуля при добавлении 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, потому что разлив регистров увеличит объем кэша кода.
2 ответа
Да, вы должны предположить, что старшие 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 не говорит, что что-то указано, вы не можете полагаться на это".
Он также подтвердил, что, например, 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 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 undefined намеренно, и я думаю, что это хорошее дизайнерское решение.
Игнорирование старшего 32 свободно при выполнении 32-битных операций. 32-битная операция ноль расширяет свой результат до 64-битного бесплатно, поэтому вам нужно только дополнительное mov edx, edi
или что-то еще, если бы вы могли использовать reg непосредственно в 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 были расширены до 32 b для передачи аргументов, имеет некоторый смысл. См. Почему GCC не использует частичные регистры? для получения более подробной информации об этих микроархитектурах.
Это, вероятно, не имеет большого значения для размера кода в реальном коде, поскольку крошечные функции static inline
и insns для обработки аргументов - небольшая часть больших функций. Межпроцедурная оптимизация может устранить издержки между вызовами, когда компилятор может видеть оба определения, даже без вставки. (ИДК, насколько хорошо это делают компиляторы на практике.)
Я не уверен, стоит ли менять сигнатуры функций для использования 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-родственником + немедленным, потому что на самом деле важен счетчик числа операций переднего плана, а не счетчик команд.)
Как указывает комментарий EOF, компилятор не может предположить, что старшие 32 бита 64-битного регистра, используемого для передачи 32-битного аргумента, имеют какое-то конкретное значение. Это делает необходимым расширение знака или нуля.
Единственный способ предотвратить это - использовать в качестве аргумента 64-битный тип, но это переносит требование по расширению значения до вызывающей стороны, что может и не быть улучшением. Я бы не стал слишком беспокоиться о размерах разливов регистров, поскольку, как вы делаете это сейчас, вероятно, более вероятно, что после расширения исходное значение будет мертвым, и будет разлито 64-разрядное расширенное значение, Даже если он не умер, компилятор все же может предпочесть разлить 64-битное значение.
Если вы действительно беспокоитесь о своем объеме памяти и вам не нужно большее 64-битное адресное пространство, вы можете взглянуть на x32 ABI, который использует типы ILP32, но поддерживает полный набор 64-битных инструкций.