Вычесть и обнаружить недополнение, самый эффективный способ? (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 очень хорош в планировании и подобных низкоуровневых оптимизациях.

Другие вопросы по тегам