N отрицательный, положительный или нулевой? возврат 1, 2 или 4

Я строю интерпретатор PowerPC, и он работает очень хорошо. В архитектуре Power регистр условий CR0 (EFLAGS на x86) обновляется практически для любой команды. Он установлен так. Значение CR0 равно 1, если последний результат был отрицательным, 2, если последний результат был положительным, 4 в противном случае.

Мой первый наивный метод для интерпретации:

if (n < 0)
    cr0 = 1
else if (n > 0)
    cr0 = 2;
else
    cr0 = 4;

Однако я понимаю, что все эти ветки не будут оптимальными, и будут выполняться миллионы раз в секунду. Я видел, как немного взломали SO, но никто из них не был уверен. Например, я нашел много примеров для преобразования числа в -1, 0 или 1 соответственно знаку или 0. Но как сделать -1 = 1, 1 = 2, 0 = 4? Я прошу помощи Бит Хакеры...

Заранее спасибо

Update: Прежде всего: спасибо, ребята, вы были здоровы. Я тщательно проверю все ваши коды на скорости, и вы будете первыми, кто узнает, кто победитель.

@jalf: О вашем первом совете я фактически не вычислял CR0 в каждой инструкции. Я скорее сохранил переменную lastResult, и когда (и если) следующая инструкция запросила флаг, выполните сравнение. Три основных мотивации вернули меня к обновлению "everytime":

  • В PPC вы не обязаны обновлять CR0, как на x86 (где ADD всегда меняет EFLAGS, даже если это не нужно), у вас есть два варианта ADD, одно обновление. Если компилятор хочет использовать обновление, это означает, что он будет использовать CR0 в какой-то момент, поэтому нет смысла задерживать...
  • Там особенно болезненная инструкция называется mtcrf, которая позволяет вам произвольно изменить CR0. Вы даже можете установить его на 7, без арифметического значения... Это просто разрушает возможность сохранения переменной lastResult.
33
задан 04 марта '12 в 23:33
источник поделиться
8 ответов

Во-первых, если эта переменная должна обновляться после (почти) каждой инструкции, очевидная рекомендация такова:

не

Обновляйте его только тогда, когда последующие инструкции нуждаются в его значении. В любой другой момент нет смысла его обновлять.

Но в любом случае, когда мы его обновляем, мы хотим, чтобы это поведение:

R < 0  => CR0 == 0b001 
R > 0  => CR0 == 0b010
R == 0 => CR0 == 0b100

В идеале нам вообще не нужно встраиваться. Вот один из возможных подходов:

  • Установите CR0 на значение 1. (если вы действительно хотите скорость, выясните, можно ли это сделать, не выбирая константу из памяти. Даже если вам придется потратить пару инструкций на нее, это может стоить того)
  • Если R >= 0, сдвиг влево на один бит.
  • Если R == 0, сдвиг влево на один бит

Если шаги 2 и 3 могут быть преобразованы для исключения части "if"

CR0 <<= (R >= 0);
CR0 <<= (R == 0);

Это быстрее? Я не знаю. Как всегда, когда вас беспокоит производительность, вам нужно измерить, измерить, измерить.

Однако я вижу несколько преимуществ этого подхода:

  • мы полностью избегаем ветвей.
  • мы избегаем загрузки/хранения памяти.
  • инструкции, на которые мы полагаемся (смещение и сравнение бит) должны иметь низкую задержку, что не всегда относится к умножению, например.

Недостатком является то, что у нас есть цепочка зависимостей между всеми тремя строками: каждый изменяет CR0, который затем используется в следующей строке. Это несколько ограничивает уровень инструкций parallelism.

Чтобы свести к минимуму эту цепочку зависимостей, мы могли бы сделать что-то вроде этого:

CR0 <<= ((R >= 0) + (R == 0));

поэтому нам нужно только один раз изменить CR0 после его инициализации.

Или, делая все в одной строке:

CR0 = 1 << ((R >= 0) + (R == 0));

Конечно, существует множество возможных вариантов этой темы, поэтому идите и экспериментируйте.

33
ответ дан 04 марта '12 в 23:59
источник

Множество ответов, которые примерно "не делают" уже, как обычно:) Вы хотите, чтобы бит взломал? Ты получишь это. Тогда не стесняйтесь использовать его или нет по своему усмотрению.

Вы можете использовать это сопоставление для -1, 0 и 1 (sign), а затем выполните следующее:

return 7 & (0x241 >> ((sign(x) + 1) * 4));

Что по сути использует крошечную таблицу поиска.

Или "наивный битак":

int y = ((x >> 31) & 1) | ((-x >> 31) & 2)
return (~(-y >> 31) & 4) | y;

Первая строка отображает x < 0 в 1, x > 0 в 2 и x == 0 в 0. Вторая строка затем отображает y == 0 в 4 и y != 0 в y.


И, конечно, у него есть скрытый крайный кейс для x = 0x80000000, который сопоставляется с 3. Oops. Хорошо, исправьте, что:

int y = ((x >> 31) & 1) | ((-x >> 31) & 2)
y &= 1 | ~(y << 1);  // remove the 2 if odd
return (~(-y >> 31) & 4) | y;
20
ответ дан 04 марта '12 в 23:48
источник

Следующее выражение немного загадочно, но не слишком сильно, и похоже, что компилятор может легко оптимизировать:

cr0 = 4 >> ((2 * (n < 0)) + (n > 0));

Здесь, какой GCC 4.6.1 для целевого объекта x86 скомпилирует его с помощью -O2:

xor ecx, ecx
mov eax, edx
sar eax, 31
and eax, 2
test    edx, edx
setg    cl
add ecx, eax
mov eax, 4
sar eax, cl

И VC 2010 с /Ox выглядит довольно похоже:

xor ecx, ecx
test eax, eax
sets cl
xor edx, edx
test eax, eax
setg dl
mov eax, 4
lea ecx, DWORD PTR [edx+ecx*2]
sar eax, cl

Версия с использованием тестов if компилируется для сборки, которая использует переходы с любым из этих компиляторов. Конечно, вы никогда не будете уверенны в том, что какой-либо конкретный компилятор будет делать с любым конкретным битом кода, который вы выберете, если вы действительно не изучите вывод. Мое выражение достаточно загадочно, что, если бы он не был действительно критичным для производительности кодом, я мог бы по-прежнему работать с версией оператора if. Поскольку вам нужно часто устанавливать регистр CR0, я думаю, что стоит оценить, действительно ли это выражение помогает.

6
ответ дан 05 марта '12 в 2:17
источник

Я работал над этим, когда мой компьютер разбился.

int cr0 = (-(n | n-1) >> 31) & 6;
cr0 |= (n >> 31) & 5;
cr0 ^= 4;

Здесь результирующая сборка (для Intel x86):

PUBLIC  ?tricky@@YAHH@Z                                 ; tricky
; Function compile flags: /Ogtpy
_TEXT   SEGMENT
_n$ = 8                                                 ; size = 4
?tricky@@YAHH@Z PROC                                    ; tricky
; Line 18
        mov     ecx, DWORD PTR _n$[esp-4]
        lea     eax, DWORD PTR [ecx-1]
        or      eax, ecx
        neg     eax
        sar     eax, 31                                 ; 0000001fH
; Line 19
        sar     ecx, 31                                 ; 0000001fH
        and     eax, 6
        and     ecx, 5
        or      eax, ecx
; Line 20
        xor     eax, 4
; Line 22
        ret     0
?tricky@@YAHH@Z ENDP                                    ; tricky

И полный исчерпывающий тест, который также достаточно подходит для бенчмаркинга:

#include <limits.h>

int direct(int n)
{
    int cr0;
    if (n < 0)
        cr0 = 1;
    else if (n > 0)
        cr0 = 2;
    else
        cr0 = 4;
    return cr0;
}

const int shift_count = sizeof(int) * CHAR_BIT - 1;
int tricky(int n)
{
    int cr0 = (-(n | n-1) >> shift_count) & 6;
    cr0 |= (n >> shift_count) & 5;
    cr0 ^= 4;
    return cr0;
}

#include <iostream>
#include <iomanip>
int main(void)
{
    int i = 0;
    do {
        if (direct(i) != tricky(i)) {
            std::cerr << std::hex << i << std::endl;
            return i;
        }
    } while (++i);
    return 0;
}
4
ответ дан 05 марта '12 в 1:18
источник

gcc без оптимизации

        movl    %eax, 24(%esp)  ; eax has result of reading n
        cmpl    $0, 24(%esp)
        jns     .L2
        movl    $1, 28(%esp)
        jmp     .L3
.L2:
        cmpl    $0, 24(%esp)
        jle     .L4
        movl    $2, 28(%esp)
        jmp     .L3
.L4:
        movl    $4, 28(%esp)
.L3:

С -O2:

        movl    $1, %edx       ; edx = 1
        cmpl    $0, %eax
        jl      .L2            ; n < 0
        cmpl    $1, %eax       ; n < 1
        sbbl    %edx, %edx     ; edx = 0 or -1
        andl    $2, %edx       ; now 0 or 2
        addl    $2, %edx       ; now 2 or 4
.L2:
        movl    %edx, 4(%esp)

Я не думаю, что вы, вероятно, сделаете гораздо лучше

3
ответ дан 05 марта '12 в 0:03
источник

Если есть более быстрый метод, возможно, компилятор уже использует его.

Держите ваш код коротким и простым; что делает оптимизатор наиболее эффективным.

Простое прямое решение на удивление хорошо работает:

cr0 = n? (n < 0)? 1: 2: 4;

x86 Assembly (выпущено VС++ 2010, flags /Ox):

PUBLIC  ?tricky@@YAHH@Z                                 ; tricky
; Function compile flags: /Ogtpy
_TEXT   SEGMENT
_n$ = 8                                                 ; size = 4
?tricky@@YAHH@Z PROC                                    ; tricky
; Line 26
        mov     eax, DWORD PTR _n$[esp-4]
        test    eax, eax
        je      SHORT $LN3@tricky
        xor     ecx, ecx
        test    eax, eax
        setns   cl
        lea     eax, DWORD PTR [ecx+1]
; Line 31
        ret     0
$LN3@tricky:
; Line 26
        mov     eax, 4
; Line 31
        ret     0
?tricky@@YAHH@Z ENDP                                    ; tricky
1
ответ дан 04 марта '12 в 23:39
источник

Для совершенно неспортивного подхода мне интересно, может ли это иметь какую-либо выгоду от скорости:

void func(signed n, signed& cr0) {
    cr0 = 1 << (!(unsigned(n)>>31)+(n==0));
}

mov         ecx,eax  ;with MSVC10, all optimizations except inlining on.
shr         ecx,1Fh  
not         ecx  
and         ecx,1  
xor         edx,edx  
test        eax,eax  
sete        dl  
mov         eax,1  
add         ecx,edx  
shl         eax,cl  
mov         ecx,dword ptr [cr0]  
mov         dword ptr [ecx],eax  

по сравнению с вашим кодом на моей машине:

test        eax,eax            ; if (n < 0)
jns         func+0Bh (401B1Bh)  
mov         dword ptr [ecx],1  ; cr0 = 1;
ret                            ; cr0 = 2; else cr0 = 4; }
xor         edx,edx            ; else if (n > 0)
test        eax,eax  
setle       dl  
lea         edx,[edx+edx+2]  
mov         dword ptr [ecx],edx ; cr0 = 2; else cr0 = 4; }
ret  

Я не очень разбираюсь в сборке, поэтому я не могу точно сказать, будет ли это иметь какую-либо выгоду (или даже если у меня есть какие-либо прыжки. В любом случае, никаких инструкций, начинающихся с j, я не вижу). Как всегда, (и, как говорили все остальные миллионы раз) ПРОФИЛЬ.

Я сомневаюсь, что это быстрее, чем сказать Jalf или Ben's, но я не видел никого, кто воспользовался тем, что на x86 все отрицательные числа имеют определенный бит, и я решил, что я выброшу его.

[EDIT] BenVoigt предлагает cr0 = 4 >> ((n != 0) + (unsigned(n) >> 31)); удалить логическое отрицание, и мои тесты показывают, что это значительное улучшение.

1
ответ дан 05 марта '12 в 10:15
источник

Вот моя попытка.

int cro = 4 >> (((n > 0) - (n < 0)) % 3 + (n < 0)*3);
-1
ответ дан 05 марта '12 в 0:34
источник

Другие вопросы по меткам или Задайте вопрос