#c #algorithm #bit-manipulation #built-in #greatest-common-divisor
#c #алгоритм #манипулирование битами #встроенный #наибольший общий делитель
Вопрос:
у clang и GCC есть int __builtin_ctz(unsigned)
функция. Это подсчитывает конечные нули в целом числе. В статье Википедии об этом семействе функций упоминается, что двоичный алгоритм GCD можно ускорить с помощью __builtin_ctz
, но я не понимаю, как.
Пример реализации двоичного GCD выглядит следующим образом:
unsigned int gcd(unsigned int u, unsigned int v)
{
// simple cases (termination)
if (u == v)
return u;
if (u == 0)
return v;
if (v == 0)
return u;
// look for factors of 2
if (~u amp; 1) // u is even
if (v amp; 1) // v is odd
return gcd(u >> 1, v);
else // both u and v are even
return gcd(u >> 1, v >> 1) << 1;
if (~v amp; 1) // u is odd, v is even
return gcd(u, v >> 1);
// reduce larger argument
if (u > v)
return gcd(u - v, v);
return gcd(v - u, u);
}
Мое подозрение заключается в том, что я мог бы использовать __builtin_ctz
следующим образом:
constexpr unsigned int gcd(unsigned int u, unsigned int v)
{
// simplified first three ifs
if (u == v || u == 0 || v == 0)
return u | v;
unsigned ushift = __builtin_ctz(u);
u >>= ushift;
unsigned vshift = __builtin_ctz(v);
v >>= vshift;
// Note sure if max is the right approach here.
// In the if-else block you can see both arguments being rshifted
// and the result being leftshifted only once.
// I expected to recreate this behavior using max.
unsigned maxshift = std::max(ushift, vshift);
// The only case which was not handled in the if-else block before was
// the odd/odd case.
// We can detect this case using the maximum shift.
if (maxshift != 0) {
return gcd(u, v) << maxshift;
}
return (u > v) ? gcd(u - v, v) : gcd(v - u, u);
}
int main() {
constexpr unsigned result = gcd(5, 3);
return resu<
}
К сожалению, это пока не работает. Программа выдает результат 4, когда он должен быть равен 1. Так что же я делаю не так? Как я могу правильно использовать __builtin_ctz
здесь? Смотрите пока мой код на GodBolt.
Комментарии:
1. Другой образец двоичного GCD там, по сути, имеет
min
то же самое, что и у васmax
, но в целом он работает немного по-другому2. Как это сравнивается с
std::gcd
? Это должно быть быстрее?3. @TedLyngmo не уверен. Я попытался сравнить его , но мой тест не удался. Обратите внимание, что ссылка на Godbolt, потому что quick-bench не позволяет вам делиться неудачными тестами. Вы знаете, что не так с этим тестом? На самом деле это не должно быть быстрее в любом случае, вопрос скорее в понимании того, как использовать CTZ.
4.Это stackoverflow в вашем
gcd
. Это переходит в очень глубокую рекурсию @u=3508125240
,v=2952784951
. Вотmax
,min
иshift
значения.5. @TedLyngmo спасибо за разъяснение этого. Моя реализация быстрее, чем
std::gcd
а у Бреттейла еще быстрее. (смотрите Мой ответ для результатов теста)
Ответ №1:
Вот моя итеративная реализация из комментариев:
Хотя алгоритмы с хвостовой рекурсией часто элегантны, итеративные реализации почти всегда быстрее на практике. (Современные компиляторы действительно могут выполнять это преобразование в очень простых случаях.)
unsigned ugcd (unsigned u, unsigned v)
{
unsigned t = u | v;
if (u == 0 || v == 0)
return t; /* return (v) or (u), resp. */
int g = __builtin_ctz(t);
while (u != 0)
{
u >>= __builtin_ctz(u);
v >>= __builtin_ctz(v);
if (u >= v)
u = (u - v) / 2;
else
v = (v - u) / 2;
}
return (v << g); /* scale by common factor. */
}
Как уже упоминалось, |u - v| / 2
шаг обычно реализуется как очень эффективный, безусловный сдвиг вправо, например, shr r32
для деления на (2)
— поскольку оба (u)
, (v)
нечетны и, следовательно, |u - v|
должны быть четными.
Это не строго необходимо, так как шаг ‘oddifying’: u >>= __builtin_clz(u);
эффективно выполнит эту операцию на следующей итерации.
Предположим, что (u)
или (v)
имеют «случайное» распределение битов, вероятность (n)
конечных нулей через tzcnt
равна ~ (1/(2^n))
. Эта инструкция является улучшением по сравнению с bsf
реализацией для __builtin_clz
, предшествующей Haswell, IIRC.
Комментарии:
1. Например, вывод ARM этого алгоритма, где он может использовать rev / clz для обработки ctz.
2. быстрая проверка: clang , gcc — выглядит действительно красиво.
3. 64-битная версия превосходит
std::gcd
еще в большей степени. Хотя на Intel Ice Lake результаты могут отличаться, это улучшает скорость деления.4.
tzcnt
имеет ложные зависимости — фактически перечисленные как ошибки — часто вставляяxor r, r
, чтобы сломать их. Я не уверен, исправила ли это более поздняя микроархитектура. однакоbsf
он значительно улучшен, поэтому многие компиляторы возвращаются к нему.5. @harold — ну, тогда мы можем просто использовать:
return ((v == 0) ? u : u64_gcd(v, u % v));
… но я читал, что разделение получило серьезный импульс в Icelake.
Ответ №2:
Благодаря полезным комментаторам, я нашел критическую ошибку: Я должен был использовать min
вместо max
Это окончательное решение:
#include <algorithm>
constexpr unsigned gcd(unsigned u, unsigned v)
{
if (u == v || u == 0 || v == 0)
return u | v;
// effectively compute min(ctz(u), ctz(v))
unsigned shift = __builtin_ctz(u | v);
u >>= __builtin_ctz(u);
v >>= __builtin_ctz(v);
const auto amp;[min, max] = std::minmax(u, v);
return gcd(max - min, min) << shift;
}
int main() {
constexpr unsigned g = gcd(25, 15); // g = 5
return g;
}
Это решение также имеет очень хороший, почти безветвленный результат компиляции.
Вот некоторые результаты тестирования всех ответов на данный момент (мы на самом деле превзошли std::gcd
):
Комментарии:
1. Кстати, вы могли бы взять минимальное значение конечного нуля с
__builtin_ctz(u | v)
2. Использование max вместо min было ошибкой. Но это нормально — использовать индивидуальные смены. Потому что дополнительные множители 2, которые вы удалили из одного из них, не меняют наибольший ОБЩИЙ делитель.
3. Если я беру 2 нечетных числа и умножаю одно на степень 2, я не изменил gcd.
4. Это «без ветвей», только если вы не включаете стоимость накладных расходов на рекурсивный вызов. В качестве хвостового рекурсивного алгоритма его относительно легко преобразовать в итеративную реализацию .
5. Не связанный: вы можете получить ссылки на
min
иmax
значения, подобные этому:const autoamp;[min, max] = std::minmax(u, v);