Можно ли создать в виде макроса своего рода целочисленное преобразование с нулевой стоимостью с помощью встроенной сборки?
Во многих случаях мне хотелось бы передавать 32-битные целые числа, которые уже «на месте», через интерфейсы, принимающие 64-битные целые числа. Но C не любит оставлять старшие биты неопределенными, а приведение или попытка расширить 32 бита до 64 с помощью объединения заставляет компиляторы либо расширять ноль, либо расширять знак 32-битного числа.
Фиктивный пример:
void g(unsigned long );
void f(int X){
unsigned long XX = (unsigned)X; //mov %edi, %edi
g(XX);
}
Можно ли заменить гипс на какой-нибудь JUST_PRETEND_ITS_AN_UNSIGNED_LONG_WITHOUT_DOING_ANYTHING(X)
?
@SimonGoater Ни то, ни другое. Оставьте верхние биты такими, какими они были.
Есть проблемы с переносимостью, но вы можете сделать это с помощью объединения элементов int x[2] и unsigned long.
@SimonGoater Как я уже сказал в вопросе, союзы, которые я уже пробовал: godbolt.org/z/WTrdx34Tf => нулевое расширение.
По моему предложению вам нужно создать массив int в объединении. Вы это пробовали?
То есть функция принимает беззнаковый длинный номер, но игнорирует старшие 32 бита, поэтому их можно оставить неопределенными?
@Бармар Правильно. Я использую longs (uintptr_t) просто как хранилище контекста для некоторых обратных вызовов. Некоторые обратные вызовы используют полные 64 бита. Другие понизят его до 32-битного, поэтому для них вполне нормально, что старшие биты будут мусором.
Сделать f
(как показано) с помощью __attribute__((always_inline))
? AFAICT, это настолько быстро, насколько это возможно. При использовании inline вызывающая сторона f
может генерировать 32-битные значения напрямую. В любом случае, разве речь не идет об одном ассемблерном инсте?
Что заставляет вас думать, что здесь можно сэкономить? Или, если бы они были, этого было бы достаточно, чтобы оправдать любые усилия в этом направлении?
Обратите внимание, что все аргументы передаются по значению, поэтому в любом случае будет выполнено копирование. На большинстве платформ это будет в регистре, и в представленном случае я ожидаю, что достойный компилятор уже будет использовать эффективный (вероятно, самый эффективный) механизм загрузки 64-битного регистра с 32-битным значением. , с расширением знака или без него, как того требует комбинация типов данных C. Я не вижу причин ожидать, что вы сможете добавить код (даже сборку), чтобы сделать его более эффективным.
@JohnBollinger Отличный вопрос. Я опубликовал супер-пустышный пример. На самом деле у меня есть много того и этого, каждый из которых принимает шесть аргументов контекста uintptr_t, и это организовано таким образом, чтобы сохранить размер кода, и компилятор немного испортил это с этими нулевыми расширениями, которые складываются. Возможно, я не буду использовать то, что нашел, но было интересно исследовать это. :D Опубликовал свою находку благодаря тем, кто проголосовал за этот вопрос. Я был готов удалить его из-за отрицательного голоса. :)
@JohnBollinger В коде, с которым я работаю, все уже организовано для того, чтобы аргументы были на месте (я беру указатели на функции в качестве последних аргументов), поэтому, если нет знака/расширяющего ноль mov, не будет никаких mov => экономия кода <=> то, что мне было нужно.
При использовании inline вызывающий f
должен вычислить значение X
. Скажем, это значение находится в eax/rax
. Надо это поставить edi/rdi
. Это делается с помощью mov*
inst. Он выяснит, является ли mov
простым, расширяющимся со знаком или расширяющимся с нуля. Это нулевые накладные расходы. Абонент [вероятно] должен сгенерировать это mov
в любом случае
@PetrSkocik, мне трудно представить ситуацию, которую ты описываешь. Если пример, представленный в вопросе, не соответствует вашему реальному случаю, это, вероятно, означает, что вы задали неправильный вопрос. Я все еще не думаю, что вы добьетесь большего, не отказавшись от C полностью (и, возможно, даже тогда), но я вполне уверен, что вы не получите ответ, который ищете, с помощью вопроса в его нынешней формулировке.
Просто объявите f
, чтобы получить unsigned long
. Тогда компилятору не придется «притворяться», что это беззнаковый длинный тип. это беззнаковый длинный.
В качестве альтернативы вашему профсоюзному коду, как насчет asm volatile ("jmp g\n");
, поскольку именно его вы пытаетесь создать. В конце может быть дополнительный (не выполненный) ret
, но кого это волнует?
@CraigEstey Есть много способов добиться этого кода. Я искал только то, о чем задавался вопрос, потому что это сочетается с другими вещами. Ответ, который я опубликовал, достаточно отвечает на этот вопрос.
Опять же, речь идет об исключении одного mov
инста. Что бы g
ни делал, сейчас он находится под огромным давлением, чтобы не потратить сэкономленные средства напрасно, введя хоть один дополнительный момент ;-)
Я бы [снова] спросил, может ли f
быть встроенным?
@CraigEstey фиктивные примеры! = реальный код. И нет, встраивание неприменимо/не полезно в моем случае использования.
Тогда у вас здесь нет MRE для вашего реального кода. Нам потребуются примеры всех соответствующих g
функций с соответствующими f
вариантами. На мой взгляд, вам нужны разные f
версии, которые вызывают желаемое поведение: без изменений, расширение знака, расширение нуля и т. д. Затем мы могли бы предложить способы их дальнейшей оптимизации/улучшения, чего вы и хотели, я полагаю.
@JohnBollinger язык указывает, что в исходном коде старшим 32 битам параметра должны быть присвоены значения. Компилятор не может это оптимизировать. (Ну, теоретически он мог бы проанализировать вызываемую функцию, чтобы увидеть, используются ли биты, но на самом деле компиляторы этого делать не собираются).
Конечно, @М.М. Когда я сказал «с расширением знака или без него, как того требует комбинация типов данных C», я имел в виду расширение знака, а не нулевое расширение, а не «сохранять случайные биты». Как это обычно бывает в C, семантика определяется в терминах значений, а не их представлений.
Вы можете использовать опцию "<digit>"
встроенного ассемблера gcc, чтобы указать, что входные и выходные данные должны быть одним и тем же регистром, а затем ничего не делать: void f(int X) { unsigned long XX; __asm__("" : "=r"(XX) : "0" (X)); g(XX); }
Это автоматически адаптируется к случаю, когда параметр передается в стек или если выравнивание или Ограничение упаковки предотвращает совмещение этих двух значений.
@RaymondChen Спасибо. Да. Ограничение <digit>
— это то, что я искал. (Я понял это вчера и опубликовал это как ответ на случай, если другие захотят этого)
Комбинация объединений и ограничений соответствия встроенных сборок, похоже, сделала это:
#include <stdint.h>
void takeUptr(uintptr_t U);
#define upcast(X) ({ union { int x; uintptr_t xx; } u; u.x = X; \
asm("":"=r"(u.xx):"0"(u.x)); /*make C think the upper bits are inited*/ \
u.xx; })
void passAsUptr(int x){ takeUptr(upcast(x)); }
//^jmp takeUptr; //no zero/sign-extending mov ✓
uintptr_t retAsUptr(int x){ return upcast(x); }
//^movl %edi, %eax; ret; //correctly moved to the output register ✓
https://godbolt.org/z/vz9W5Mq8M
В gcc (к сожалению, не в clang) это устраняет mov, расширяющий ноль при переходе от
от 32-битного до 64-битного числа, если они оба сопоставлены с одним и тем же регистром. Ключевым моментом было использование позиционного ограничения 0, чтобы регистр ввода (здесь u.x
) соответствовал регистру вывода (u.xx
, в позиции 0, следовательно, ограничение 0
).
Код неправильный. Проблема в том, что на самом деле нет гарантии, что верхние биты %rdi чисты.
@Джошуа И? Я не хочу, чтобы они были ясными. Я хочу, чтобы они были такими, какими они были, не заставляя компилятор вставлять mov, расширяющий ноль/знак. В этом весь смысл.
Мгновенно -2, не принимать никаких обязательств при проверке кода.
@n.m.couldbeanAI :D Ну, его всегда можно предварительно подготовить, инкапсулировать и использовать обычное приведение на платформах, отличных от x86-64. Но мне приятно знать, что есть простой способ сделать эту очень специфическую микроштуку :).
Когда u.xx
шире, чем u.x
, неясно, какие 32-битные из 64-битных u.xx
установлены: верхняя или нижняя половина. Это делает код менее переносимым и требует большего обслуживания.
Я думаю, н.м. найдите настоящую причину [не делать этого]. Для объяснения потребовался бы абзац комментариев/документации (со всеми обоснованиями против представленных здесь контраргументов). А как насчет тех версий g
, которые маскируют/игнорируют старшие 32 бита? Предположим, через год или около того другой программист работает над g
и понимает, что может удалить маскировку в g
, потому что ожидает чистый, хорошо сформированный unsigned long
. Они никогда не увидят вашу оптимизированную f
функцию, которая дает им «грязный» unsigned long
. Скрытая ошибка? Или во всех местах требуется много комментариев?
@CraigEstey Вот более полный контекст: у меня есть функция типа long applyCallbackInContext(long,long,long,long,long, long (*callback)(long,long,long,long,long))
Tail, вызываемая из многих функций, у которых уже есть первые 5 аргументов, но во многих случаях некоторые или все из них являются не полными длинными значениями, а скорее целыми. Если бы не нулевые расширяющие перемещения, все это можно было бы сделать просто загрузкой указателя функции с последующим jmp на x86-64 (основная цель). Обратные вызовы будут возвращаться к исходным типам, поэтому любое расширение знака/ноля является просто пустой тратой, и оно накапливается из-за наличия большого количества таких оболочек.
@CraigEstey Оставлять вещи неопределенными, когда вы не собираетесь их использовать, не так уж редко и трудно понять в C. Вышеупомянутое позволяет вам делать это и для частей регистра. Используйте его или нет. Выше описано, как это можно сделать.
inline long long upcast(int v) { long long l; __asm__("" : "=r"(l) : "0" (v)); return l; }
избегает макроса.
@RaymondChen Очень хорошее улучшение. Мне нравится, как это покончило с профсоюзом. Не знаю, почему я решил, что мне это нужно. Спасибо!
Какой ты хочешь? расширение нуля или расширение знака? Где размер лонга больше, решать вам.