Бит-трюк, чтобы определить, имеет ли какое-либо из целых чисел определенное значение
Есть ли какой-нибудь умный трюк, чтобы определить, имеет ли какое-либо из небольшого числа целых чисел (скажем, 3 или 4) определенное значение?
Простой
bool test(int a, int b, int c, int d)
{
// The compiler will pretty likely optimize it to (a == d | b == d | c == d)
return (a == d || b == d || c == d);
}
в GCC компилируется в
test(int, int, int, int):
cmp ecx, esi
sete al
cmp ecx, edx
sete dl
or eax, edx
cmp edi, ecx
sete dl
or eax, edx
ret
Те команды sete
имеют более высокую задержку, чем я хочу терпеть, поэтому я предпочел бы использовать что-то побитовое (&
, |
, ^
, ~
) и одно сравнение.
Ответы
Ответ 1
Единственное, что я нашел, это:
int s1 = ((a-d) >> 31) | ((d-a) >> 31);
int s2 = ((b-d) >> 31) | ((d-b) >> 31);
int s3 = ((c-d) >> 31) | ((d-c) >> 31);
int s = s1 & s2 & s3;
return (s & 1) == 0;
альтернативный вариант:
int s1 = (a-d) | (d-a);
int s2 = (b-d) | (d-b);
int s3 = (c-d) | (d-c);
int s = (s1 & s2 & s3);
return (s & 0x80000000) == 0;
оба переведены на:
mov eax, ecx
sub eax, edi
sub edi, ecx
or edi, eax
mov eax, ecx
sub eax, esi
sub esi, ecx
or esi, eax
and esi, edi
mov eax, edx
sub eax, ecx
sub ecx, edx
or ecx, eax
test esi, ecx
setns al
ret
который имеет меньше заданных команд, но, очевидно, больше mov/sub.
Обновление: как предлагалось BeeOnRope @- имеет смысл вводить входные переменные в unsigned
Ответ 2
Это не полный трюк. Любой нуль дает нулевое произведение, которое дает нулевой результат. Отрицание 0 дает 1. Не имеет дело с переполнением.
bool test(int a, int b, int c, int d)
{
return !((a^d)*(b^d)*(c^d));
}
gcc 7.1 -O3
. (d
находится в ecx
, другие входы начинаются в других целочисленных регистрах).
xor edi, ecx
xor esi, ecx
xor edx, ecx
imul edi, esi
imul edx, edi
test edx, edx
sete al
ret
Это может быть быстрее, чем оригинал на Core2 или Nehalem, где проблема частичных регистрационных столов. imul r32,r32
имеет 3c латентность на Core2/Nehalem (и более поздние процессоры Intel) и 1 на пропускную способность каждого тактового сигнала, поэтому эта последовательность имеет 7-секундную задержку от входов до второго результата imul
и еще 2 цикла задержки для test
/sete
. Пропускная способность должна быть достаточно хорошей, если эта последовательность работает на нескольких независимых входах.
Использование 64-битного умножения позволит избежать проблемы переполнения при первом умножении, но второй может все еще переполняться, если сумма равна >= 2**64
. Это все равно будет такая же производительность для Intel Nehalem и семейства Sandybridge, и AMD Ryzen. Но это было бы медленнее на более старых процессорах.
В x86 asm выполнение второго умножения с помощью полноразмноженной команды с одним операндом mul
(64x64b = > 128b) позволит избежать переполнения, и результат может быть проверен на то, чтобы быть полностью нулевым или нет с помощью or rax,rdx
, Мы можем записать это в GNU C для 64-битных целей (где __int128
доступно)
bool test_mulwide(unsigned a, unsigned b, unsigned c, unsigned d)
{
unsigned __int128 mul1 = (a^d)*(unsigned long long)(b^d);
return !(mul1*(c^d));
}
и gcc/clang действительно испускают asm, на который мы надеялись (каждый с некоторыми бесполезными инструкциями mov
):
# gcc -O3 for x86-64 SysV ABI
mov eax, esi
xor edi, ecx
xor eax, ecx
xor ecx, edx # zero-extends
imul rax, rdi
mul rcx # 64 bit inputs (rax implicit), 128b output in rdx:rax
mov rsi, rax # this is useless
or rsi, rdx
sete al
ret
Это должно быть почти так же быстро, как простая версия, которая может переполняться, на современном x86-64. (mul r64
по-прежнему остается только 3 c латентность, но 2 uops вместо 1 для imul r64,r64
, который не дает высокую половину), в семействе Intel Sandybridge.)
Это еще хуже, чем clang setcc
/or
вывод из исходной версии, в котором используются 8-разрядные инструкции or
, чтобы избежать чтения 32-разрядных регистров после записи младшего байта (т.е.).
Посмотрите оба источника с обоими компиляторами в проводнике компилятора Godbolt. (Также включена версия @BeeOnRope ^
/&
, которая подвергает риску ложные срабатывания, с полной отдачей и без нее.)
Ответ 3
return !(a^d && b^d && c^d);
может показаться приятным трюком, но не гарантированно будет более эффективным.
Если два целых числа одинаковы, то их xor равно нулю.