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.
Ответы
Ответ 1
Во-первых, если эта переменная должна обновляться после (почти) каждой инструкции, очевидная рекомендация такова:
не
Обновляйте его только тогда, когда последующие инструкции нуждаются в его значении. В любой другой момент нет смысла его обновлять.
Но в любом случае, когда мы его обновляем, мы хотим, чтобы это поведение:
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));
Конечно, существует множество возможных вариантов этой темы, поэтому идите и экспериментируйте.
Ответ 2
Множество ответов, которые примерно "не делают" уже, как обычно:) Вы хотите, чтобы бит взломал? Ты получишь это. Тогда не стесняйтесь использовать его или нет по своему усмотрению.
Вы можете использовать это сопоставление для -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;
Ответ 3
Следующее выражение немного загадочно, но не слишком сильно, и похоже, что компилятор может легко оптимизировать:
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, я думаю, что стоит оценить, действительно ли это выражение помогает.
Ответ 4
Я работал над этим, когда мой компьютер разбился.
int cr0 = (-(n | n-1) >> 31) & 6;
cr0 |= (n >> 31) & 5;
cr0 ^= 4;
Здесь результирующая сборка (для Intel x86):
PUBLIC [email protected]@[email protected] ; tricky
; Function compile flags: /Ogtpy
_TEXT SEGMENT
_n$ = 8 ; size = 4
[email protected]@[email protected] 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
[email protected]@[email protected] 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;
}
Ответ 5
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)
Я не думаю, что вы, вероятно, сделаете гораздо лучше
Ответ 6
Если есть более быстрый метод, возможно, компилятор уже использует его.
Держите ваш код коротким и простым; что делает оптимизатор наиболее эффективным.
Простое прямое решение на удивление хорошо работает:
cr0 = n? (n < 0)? 1: 2: 4;
x86 Assembly (выпущено VС++ 2010, flags /Ox
):
PUBLIC [email protected]@[email protected] ; tricky
; Function compile flags: /Ogtpy
_TEXT SEGMENT
_n$ = 8 ; size = 4
[email protected]@[email protected] PROC ; tricky
; Line 26
mov eax, DWORD PTR _n$[esp-4]
test eax, eax
je SHORT [email protected]
xor ecx, ecx
test eax, eax
setns cl
lea eax, DWORD PTR [ecx+1]
; Line 31
ret 0
[email protected]:
; Line 26
mov eax, 4
; Line 31
ret 0
[email protected]@[email protected] ENDP ; tricky
Ответ 7
Для совершенно неспортивного подхода мне интересно, может ли это иметь какую-либо выгоду от скорости:
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));
удалить логическое отрицание, и мои тесты показывают, что это значительное улучшение.
Ответ 8
Вот моя попытка.
int cro = 4 >> (((n > 0) - (n < 0)) % 3 + (n < 0)*3);