Вычесть и обнаружить недополнение, самый эффективный способ? (x86/64 с GCC)
Я использую GCC 4.8.1 для компиляции кода на C, и мне нужно определить, происходит ли потеря значения при вычитании в архитектуре x86/64. Оба НЕ ПОДПИСАНЫ. Я знаю, что сборка очень проста, но мне интересно, смогу ли я сделать это в C-коде и заставить GCC оптимизировать его таким образом, потому что я не могу его найти. Это очень используемая функция (или низкоуровневый, это термин?), Поэтому мне нужно, чтобы она была эффективной, но GCC кажется слишком тупым, чтобы распознать эту простую операцию? Я пробовал так много способов дать ему подсказки в C, но он всегда использует два регистра, а не просто подпрограмму и условный переход. И, честно говоря, меня раздражает, что такой глупый код написан так много раз (функция вызывается много раз).
Мой лучший подход в Си, казалось, был следующим:
if((a-=b)+b < b) {
// underflow here
}
По сути, вычтите b из a, и если результат недостаточного значения обнаружит его и выполнит некоторую условную обработку (которая не связана со значением a, например, это приводит к ошибке и т. Д.).
GCC кажется слишком глупым, чтобы свести вышесказанное к простому и условному переходу, и поверьте мне, я пробовал так много способов сделать это в коде C и пробовал много опций командной строки (-O3 и -Os включены, конечно). GCC работает примерно так (синтаксическая сборка Intel):
mov rax, rcx ; 'a' is in rcx
sub rcx, rdx ; 'b' is in rdx
cmp rax, rdx ; useless comparison since sub already sets flags
jc underflow
Излишне говорить, что вышесказанное глупо, когда все, что ему нужно, это:
sub rcx, rdx
jc underflow
Это так раздражает, потому что GCC понимает, что sub изменяет флаги таким образом, поскольку, если я приведу его в тип "int", он сгенерирует точное выше, за исключением того, что он использует "js", то есть переход со знаком, а не перенос, который не будет работать, если разность значений без знака достаточно велика, чтобы установить старший бит. Тем не менее он показывает, что знает о подчиненной инструкции, влияющей на эти флаги.
Теперь, может быть, я должен отказаться от попыток заставить GCC оптимизировать это должным образом и сделать это с помощью встроенной сборки, с которой у меня нет проблем. К сожалению, это требует "asm goto", потому что мне нужен условный JUMP, и asm goto не очень эффективен с выходом, потому что он изменчив.
Я пытался что-то, но я понятия не имею, является ли это "безопасным" для использования или нет. ASM Goto не может иметь выходы по какой-то причине. Я не хочу, чтобы это сбрасывало все регистры в память, это убило бы весь смысл, которым я занимаюсь, это эффективность. Но если я буду использовать пустые операторы asm с выходными данными, установленными в переменную 'a' до и после нее, будет ли это работать и безопасно ли это? Вот мой макрос:
#define subchk(a,b,g) { typeof(a) _a=a; \
asm("":"+rm"(_a)::"cc"); \
asm goto("sub %1,%0;jc %l2"::"r,m,r"(_a),"r,r,m"(b):"cc":g); \
asm("":"+rm"(_a)::"cc"); }
и используя это так:
subchk(a,b,underflow)
// normal code with no underflow
// ...
underflow:
// underflow occured here
Это немного некрасиво, но работает просто отлично. В моем тестовом сценарии он компилирует только FINE без непостоянных накладных расходов (сбрасывает регистры в память), не генерируя ничего плохого, и кажется, что он работает нормально, однако это всего лишь ограниченный тест, я не могу проверить это везде, где я использую эту функцию /macro, как я сказал, используется ОЧЕНЬ МНОГО, поэтому я хотел бы знать, если кто-то осведомлен, есть ли что-то небезопасное в вышеупомянутой конструкции?
В частности, значение 'a' НЕ НУЖНО, если происходит недостаточное заполнение, поэтому, учитывая это, есть ли побочные эффекты или небезопасные вещи, которые могут произойти с моим встроенным макросом asm? Если нет, я буду использовать его без проблем, пока они не оптимизируют компилятор, чтобы я мог заменить его после того, как угадаю.
Пожалуйста, не превращайте это в дискуссию о преждевременной оптимизации или нет, оставайтесь на теме вопроса, я полностью осознаю это, так что спасибо.
3 ответа
Я, вероятно, скучаю по чему-то очевидному, но почему это не хорошо?
extern void underflow(void) __attribute__((noreturn));
unsigned foo(unsigned a, unsigned b)
{
unsigned r = a - b;
if (r > a)
{
underflow();
}
return r;
}
Я проверил, GCC оптимизирует его для того, что вы хотите:
foo:
movl %edi, %eax
subl %esi, %eax
jb .L6
rep
ret
.L6:
pushq %rax
call underflow
Конечно, вы можете справиться с недостатком, как хотите, я только что сделал это, чтобы сохранить простоту.
Как насчет следующего кода сборки (вы можете обернуть его в формат GCC):
sub rcx, rdx ; assuming operands are in rcx, rdx
setc al ; capture carry bit int AL (see Intel "setxx" instructions)
; return AL as boolean to compiler
Затем вы вызываете / вставляете код сборки и переходите к полученному логическому значению.
Вы проверяли, действительно ли это быстрее? Современные x86-микроархитектуры используют микрокод, превращая отдельные инструкции по сборке в последовательности более простых микроопераций. Некоторые из них также выполняют микрооперацию, в которой последовательность инструкций по сборке превращается в одну микрооперацию. В частности, такие последовательности, как test %reg, %reg; jcc target
Возможно, это связано с тем, что глобальные флаги процессора представляют собой проблему производительности.
Если cmp %reg, %reg; jcc target
это mOp-fused, gcc может использовать это для получения более быстрого кода. По моему опыту, gcc очень хорош в планировании и подобных низкоуровневых оптимизациях.