Оптимизируйте меня! (C, производительность) - продолжение вопроса об изменении битов

Благодаря некоторым очень полезным пользователям stackOverflow на Bit twiddling: какой бит установлен? , я создал свою функцию (опубликовано в конце вопрос).

Любые предложения - даже небольшие предложения - будут оценены. Надеюсь, это улучшит мой код, но, по крайней мере, это должно меня чему-то научить. :)

Обзор

Эта функция будет вызываться не менее 10 13 раз, а возможно, 10 15 . То есть этот код будет работать месяцев по всей вероятности, поэтому любые советы по производительности будут полезны.

На эту функцию приходится 72-77% времени программы на основе профилирования и около десятка запусков в различных конфигурациях (оптимизация некоторых параметров здесь не актуальна).

На данный момент функция работает в среднем за 50 тактов. Я не уверен, насколько это можно улучшить, но я был бы рад увидеть, как это работает через 30.

Ключевые наблюдения

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

Так я надеюсь сэкономить больше всего времени, а не за счет дальнейших микрооптимизаций (хотя они, конечно, тоже приветствуются!).

Информация о производительности

  • smallprimes - это битовый массив (64 бита); в среднем будет установлено около 8 бит, но может быть от 0 до 12.
  • q обычно будет отличным от нуля. (Обратите внимание, что функция завершается раньше, если q и малые простые числа равны нулю.)
  • r и s часто будут равны 0. Если q равно нулю, r и s тоже будут; если r равно нулю, s тоже будет.
  • Как говорится в комментарии в конце, nu обычно в конце равно 1, так что у меня есть эффективный специальный случай для него.
  • Вычисления, приведенные ниже для особого случая, могут показаться риском переполнения, но с помощью соответствующего моделирования я доказал, что с моей точки зрения этого не произойдет - так что не беспокойтесь об этом случае.
  • Функции, не определенные здесь (ugcd, minuu, star и т. Д.), Уже оптимизированы; ни один не заставит себя долго бежать. pr - небольшой массив (все в L1). Кроме того, все вызываемые здесь функции являются чистыми функциями .
  • Но если вам все равно ... ugcd - это gcd , minuu - это минимум, vals - это количество конечных двоичных нулей, __builtin_ffs - это расположение самой левой двоичной единицы, звездочка - (n -1) >> vals (n-1), pr - это массив простых чисел от 2 до 313.
  • В настоящее время вычисления выполняются на Phenom II 920 x4, хотя оптимизация для i7 или Woodcrest все еще представляет интерес. (если я получаю время вычислений на других узлах).
  • Я буду рад ответить на любые ваши вопросы о функции или ее составляющих.

Что он делает на самом деле

Добавлено в ответ на запрос. Вам не нужно читать эту часть.

Введено нечетное число n с 1

smallprimes & 1 устанавливается, если число делится на 3, smallprimes & 2 устанавливается, если число делится на 5, smallprimes & 4 устанавливается, если число делится на 7, smallprimes & 8 устанавливается, если число делится на 11 и т. д. до самого старшего бита, который представляет 313. Число, делящееся на квадрат простого числа, не представляется иначе, чем число, делимое только на это число. (Фактически, кратные квадратам можно отбросить; на этапе предварительной обработки в другой функции кратные квадратам простых чисел <= lim имеют smallprimes и q, равные 0, поэтому они будут отброшены, где оптимальное значение lim определяется экспериментально. )

q, r и s представляют собой большие множители числа. Любой оставшийся множитель (который может быть больше квадратного корня из числа или, если s не равно нулю, может быть даже меньше), может быть найден путем деления множителей из числа n.

После того, как все факторы восстановлены таким образом, количество оснований, 1 <= b сильным псевдопростом , подсчитывается с использованием математической формулы, которая лучше всего объясняется кодом.

Усовершенствования, сделанные на данный момент

  • Продвинули тест на ранний выход. Это явно экономит работу, поэтому я внес изменения.
  • Соответствующие функции уже встроены, поэтому __ attribute__ ((inline)) ничего не делает. Как ни странно, маркировка основной функции , баз и некоторых помощников атрибутом __ ((hot)) снижает производительность почти на 2%, и я не могу понять, почему (но это воспроизводимо с более 20 тестов).Так что я не делал этого изменения. Точно так же __ attribute__ ((const)) в лучшем случае не помогло. Меня это более чем немного удивило.

Код

ulong bases(ulong smallprimes, ulong n, ulong q, ulong r, ulong s)
{
    if (!smallprimes & !q)
        return 0;

    ulong f = __builtin_popcountll(smallprimes) + (q > 1) + (r > 1) + (s > 1);
    ulong nu = 0xFFFF;  // "Infinity" for the purpose of minimum
    ulong nn = star(n);
    ulong prod = 1;

    while (smallprimes) {
        ulong bit = smallprimes & (-smallprimes);
        ulong p = pr[__builtin_ffsll(bit)];
        nu = minuu(nu, vals(p - 1));
        prod *= ugcd(nn, star(p));
        n /= p;
        while (n % p == 0)
            n /= p;
        smallprimes ^= bit;
    }
    if (q) {
        nu = minuu(nu, vals(q - 1));
        prod *= ugcd(nn, star(q));
        n /= q;
        while (n % q == 0)
            n /= q;
    } else {
        goto BASES_END;
    }
    if (r) {
        nu = minuu(nu, vals(r - 1));
        prod *= ugcd(nn, star(r));
        n /= r;
        while (n % r == 0)
            n /= r;
    } else {
        goto BASES_END;
    }
    if (s) {
        nu = minuu(nu, vals(s - 1));
        prod *= ugcd(nn, star(s));
        n /= s;
        while (n % s == 0)
            n /= s;
    }

    BASES_END:
    if (n > 1) {
        nu = minuu(nu, vals(n - 1));
        prod *= ugcd(nn, star(n));
        f++;
    }

    // This happens ~88% of the time in my tests, so special-case it.
    if (nu == 1)
        return prod << 1;

    ulong tmp = f * nu;
    long fac = 1 << tmp;
    fac = (fac - 1) / ((1 << f) - 1) + 1;
    return fac * prod;
}
27
задан Community 23 May 2017 в 11:53
поделиться

13 ответов

Похоже, вы тратите много времени на деление по факторам.Намного быстрее заменить деление умножением на обратную величину делителя (деление: ~ 15-80 (! ) циклов, в зависимости от делителя, умножение: ~ 4 цикла), ЕСЛИ Конечно, вы можете предварительно вычислить обратные.

Хотя это кажется маловероятным с q , r , s - из-за диапазона этих переменных это очень легко сделать с p , который всегда берется из небольшого статического массива pr [] . Предварительно вычислите значения, обратные этим простым числам, и сохраните их в другом массиве. Затем вместо деления на p умножьте на обратную величину, взятую из второго массива. (Или создайте единый массив структур.)

Теперь, чтобы получить точный результат деления этим методом, требуется некоторая хитрость для компенсации ошибок округления. Вы найдете кровавые подробности этого метода в этом документе на странице 138.

РЕДАКТИРОВАТЬ:

После консультации с Hacker's Delight (отличная книга, BTW) по теме Субъект, похоже, вы можете сделать это еще быстрее, используя тот факт, что все подразделения в вашем коде точны (то есть остаток равен нулю).

Кажется, что для каждого делителя d , который является нечетным и базовым B = 2 word_size , существует уникальный мультипликативный обратный d⃰ , который удовлетворяет условиям: d⃰ и d · d⃰ ≡ 1 (mod B) .Для каждого x , который является точным кратным d , это означает x / d ≡ x · d⃰ (mod B) . Это означает, что вы можете просто заменить деление на умножение, без дополнительных исправлений, проверок, проблем с округлением и т. Д. (Доказательства этих теорем можно найти в книге.) Обратите внимание , что эта мультипликативная обратная величина не обязательно равна обратной величине, как определено в предыдущем методе!

Как проверить, является ли данное x точным кратным d , то есть x mod d = 0 ? Легкий! x mod d = 0 iff x · d⃰ mod B ≤ ⌊ (B-1) / d⌋ . Обратите внимание, что этот верхний предел можно вычислить заранее.

Итак, в коде:

unsigned x, d;
unsigned inv_d = mulinv(d);          //precompute this!
unsigned limit = (unsigned)-1 / d;   //precompute this!

unsigned q = x*inv_d;
if(q <= limit)
{
   //x % d == 0
   //q == x/d
} else {
   //x % d != 0
   //q is garbage
}

Предположим, что массив pr [] становится массивом struct prime :

struct prime {
   ulong p;
   ulong inv_p;  //equal to mulinv(p)
   ulong limit;  //equal to (ulong)-1 / p
}

while (smallprimes) цикл в вашем коде становится:

while (smallprimes) {
    ulong bit = smallprimes & (-smallprimes);
    int bit_ix = __builtin_ffsll(bit);
    ulong p = pr[bit_ix].p;
    ulong inv_p = pr[bit_ix].inv_p;
    ulong limit = pr[bit_ix].limit;
    nu = minuu(nu, vals(p - 1));
    prod *= ugcd(nn, star(p));
    n *= inv_p;
    for(;;) {
        ulong q = n * inv_p;
        if (q > limit)
            break;
        n = q;
    }
    smallprimes ^= bit;
}

А для функции mulinv () :

ulong mulinv(ulong d) //d needs to be odd
{
   ulong x = d;
   for(;;)
   {
      ulong tmp = d * x;
      if(tmp == 1)
         return x;
      x *= 2 - tmp;
   }
}

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

Доказательства, почему s и как s - все это доступно в книге. Настоятельно рекомендуется прочитать :-).

14
ответ дан 28 November 2019 в 05:13
поделиться

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

Кстати, у меня есть очень быстрый код для поиска факторов, которые вы используете, без какого-либо деления. Это немного похоже на решето, но очень быстро производит множители последовательных чисел . Можете найти и опубликовать, если считаете, что это может помочь.

edit пришлось воссоздать код здесь:

#include 
#define SIZE (1024*1024) //must be 2^n
#define MASK (SIZE-1)
typedef struct {
    int p;
    int next;
} p_type;

p_type primes[SIZE];
int sieve[SIZE];

void init_sieve()
{
    int i,n;
    int count = 1;
    primes[1].p = 3;
    sieve[1] = 1;
    for (n=5;SIZE>n;n+=2)
    {
        int flag = 0;
        for (i=1;count>=i;i++)
        {
            if ((n%primes[i].p) == 0)
            {
                flag = 1;
                break;
            }
        }
        if (flag==0)
        {
            count++;
            primes[count].p = n;
            sieve[n>>1] = count;
        }
    }
}

int main()
{
    int ptr,n;
    init_sieve();
    printf("init_done\n");

    // factor odd numbers starting with 3
    for (n=1;1000000000>n;n++)
    {
        ptr = sieve[n&MASK];
        if (ptr == 0) //prime
        {
//            printf("%d is prime",n*2+1);
        }
        else //composite
        {
//            printf ("%d has divisors:",n*2+1);
            while(ptr!=0)
            {
//                printf ("%d ",primes[ptr].p);
                sieve[n&MASK]=primes[ptr].next;
                //move the prime to the next number it divides
                primes[ptr].next = sieve[(n+primes[ptr].p)&MASK];
                sieve[(n+primes[ptr].p)&MASK] = ptr;
                ptr = sieve[n&MASK];
            }
        }
//        printf("\n");
    }
    return 0;
}

Функция init создает факторную базу и инициализирует решето. На моем ноутбуке это занимает около 13 секунд. Затем все числа до 1 миллиарда факторируются или определяются как простые еще через 25 секунд.Числа меньше SIZE никогда не указываются как простые, потому что они имеют 1 множитель в факторной базе, но это можно изменить.

Идея состоит в том, чтобы поддерживать связанный список для каждой записи в сите. Числа вычисляются путем простого извлечения их множителей из связанного списка. По мере извлечения они вставляются в список для следующего числа, которое будет делиться на это простое число. Это тоже очень дружелюбно к кешу. Размер сита должен быть больше самого большого простого числа в факторной базе. Как есть, это сито может работать до 2 ** 40 примерно за 7 часов, что кажется вашей целью (за исключением того, что n должно быть 64 бит).

Ваш алгоритм может быть объединен с этим, чтобы использовать факторы по мере их определения, а не упаковывать биты и большие простые числа в переменные для передачи вашей функции. Или ваша функция может быть изменена, чтобы использовать связанный список (вы можете создать фиктивную ссылку для передачи простых чисел за пределами факторной базы).

Надеюсь, это поможет.

Кстати, я впервые опубликовал этот алгоритм публично.

1
ответ дан 28 November 2019 в 05:13
поделиться

Вы пробовали выполнить поиск по таблице первого цикла while? Вы можете разделить smallprimes на 4 16-битных значения, найти их вклад и объединить их. Но, возможно, вам понадобятся побочные эффекты.

1
ответ дан 28 November 2019 в 05:13
поделиться

Вы пытались передать массив простых чисел вместо того, чтобы разбивать их на smallprimes , q , r и s ? Поскольку я не знаю, что делает внешний код, я, вероятно, ошибаюсь, но есть вероятность, что у вас также есть функция для преобразования некоторых простых чисел в растровое изображение smallprimes , и внутри этой функции вы конвертируете растровое изображение обратно в массив простых чисел. Кроме того, похоже, вы выполняете идентичную обработку для элементов smallprimes , q , r и s . Это должно сэкономить вам крошечный объем обработки за вызов.

Кроме того, вы, кажется, знаете, что переданные простые числа делят n . Достаточно ли у вас внешних знаний о степени каждого простого числа, которое делит n? Вы можете сэкономить много времени, если можете исключить операцию по модулю, передав эту информацию в эту функцию. Другими словами, если n равно pow (p_0, e_0) * pow (p_1, e_1) * ... * pow (p_k, e_k) * n_leftover , и если вы знаете подробнее об этих e_i s и n_leftover , их передача означала бы много вещей, которые вам не нужно делать в этой функции.


Может быть способ обнаружить n_leftover (не подвергнутую сектору часть n ) с меньшим количеством операций по модулю, но это всего лишь догадка, поэтому вам, возможно, придется поэкспериментировать с этим немного. Идея состоит в том, чтобы использовать gcd для многократного удаления известных множителей из n, пока вы не избавитесь от всех известных простых множителей.Позвольте мне привести почти-c-код:

factors=p_0*p_1*...*p_k*q*r*s;
n_leftover=n/factors;
do {
    factors=gcd(n_leftover, factors);
    n_leftover = n_leftover/factors;
} while (factors != 1);

Я вовсе не уверен, что это будет лучше, чем код, который у вас есть, не говоря уже о комбинированных предложениях mod / div, которые вы можете найти в других ответах, но я думаю, что это того стоит попытка. Я считаю, что это будет выигрыш, особенно для чисел с большим количеством малых простых множителей.

1
ответ дан 28 November 2019 в 05:13
поделиться

1) Я бы заставил компилятор выплюнуть генерируемую им сборку и попытаться сделать вывод, является ли то, что он делает, лучшим, что он может сделать ... и если вы обнаружите проблемы, измените код, чтобы сборка выглядит лучше. Таким образом, вы также можете убедиться, что функции, которые вы надеетесь встроить (например, star и vals), действительно встроены. (Возможно, вам придется добавить прагмы или даже превратить их в макросы)

2) Замечательно, что вы попробовали это на многоядерной машине, но этот цикл однопоточный. Я предполагаю, что существует зонтичная функция, которая распределяет нагрузку между несколькими потоками, чтобы использовать больше ядер?

3) Трудно предложить ускорение, если неясно, что фактическая функция пытается вычислить. Как правило, наиболее впечатляющее ускорение достигается не с помощью битового тидлинга, а с изменением алгоритма. Так что некоторые комментарии могут помочь; ^)

4) Если вы действительно хотите увеличить скорость на 10 * или больше, попробуйте CUDA или openCL, которые позволяют запускать программы C на вашем графическом оборудовании. Он сияет такими функциями!

5) Вы выполняете множество операций по модулю и деления сразу друг за другом. В C это 2 отдельные команды (сначала «/», а затем «%»).Однако в сборке это одна команда: 'DIV' или 'IDIV', которая возвращает как остаток, так и частное за один раз:

B.4.75 IDIV: Signed Integer Divide

IDIV r/m8                     ; F6 /7                [8086]
IDIV r/m16                    ; o16 F7 /7            [8086]
IDIV r/m32                    ; o32 F7 /7            [386]

IDIV performs signed integer division. The explicit operand provided is the divisor; the dividend and destination operands are implicit, in the following way:

For IDIV r/m8, AX is divided by the given operand; the quotient is stored in AL and the remainder in AH.

For IDIV r/m16, DX:AX is divided by the given operand; the quotient is stored in AX and the remainder in DX.

For IDIV r/m32, EDX:EAX is divided by the given operand; the quotient is stored in EAX and the remainder in EDX.

Таким образом, потребуется некоторая встроенная сборка, но я предполагаю, что это значительно ускорится. так как в вашем коде есть несколько мест, которые могут извлечь из этого выгоду.

2
ответ дан 28 November 2019 в 05:13
поделиться

Пробовали ли вы использовать оптимизацию на основе профиля?

Скомпилируйте и свяжите программу с параметром -fprofile-generate , затем запустите программу с репрезентативным набором данных (скажем, дневным объемом вычисление).

Затем перекомпилируйте и свяжите его с опцией -fprofile-use .

3
ответ дан 28 November 2019 в 05:13
поделиться

Попробуйте заменить этот шаблон (также для r и q):

n /= p; 
while (n % p == 0) 
  n /= p; 

На это:

ulong m;
  ...
m = n / p; 
do { 
  n = m; 
  m = n / p; 
} while ( m * p == n); 

В моих ограниченных тестах я получил небольшое ускорение (10%) за счет исключения модуля по модулю.

Кроме того, если p, q или r были постоянными, компилятор заменит деления умножениями. Если есть несколько вариантов для p, q или r или если некоторые из них встречаются чаще, вы можете получить что-то, специализируя функцию для этих значений.

4
ответ дан 28 November 2019 в 05:13
поделиться

Если ваш компилятор поддерживает атрибуты функций GCC, вы можете пометить свои чистые функции с помощью этого атрибута:

ulong star(ulong n) __attribute__ ((const));

Этот атрибут указывает компилятору, что результат функции зависит только от ее аргумента (ов). Эту информацию может использовать оптимизатор.

Есть ли причина, по которой вы открыли код vals () вместо использования __ builtin_ctz () ?

5
ответ дан 28 November 2019 в 05:13
поделиться

Небольшая оптимизация, но:

ulong f;
ulong nn;
ulong nu = 0xFFFF;  // "Infinity" for the purpose of minimum
ulong prod = 1;

if (!smallprimes & !q)
    return 0;

// no need to do this operations before because of the previous return
f = __builtin_popcountll(smallprimes) + (q > 1) + (r > 1) + (s > 1);
nn = star(n);

Кстати: вы должны отредактировать свое сообщение, чтобы добавить звездочку () и другие функции, которые вы используете определение

4
ответ дан 28 November 2019 в 05:13
поделиться

просто мысль, но, возможно, использование параметров оптимизации вашего компилятора поможет, если вы еще этого не сделали. другая мысль могла бы заключаться в том, что если деньги не проблема, вы можете использовать компилятор Intel C / C ++, предполагая, что вы используете процессор Intel. Я также предполагаю, что другие производители процессоров (AMD и др.) Будут иметь аналогичные компиляторы

1
ответ дан 28 November 2019 в 05:13
поделиться
  1. Убедитесь, что ваши функции получают встроенный. Если они находятся вне очереди, накладные расходы могут увеличиться, особенно в первом цикле while . Лучший способ убедиться в этом - осмотреть сборку.

  2. Вы пробовали предварительно вычислить star (pr [__ builtin_ffsll (bit)]) и vals (pr [__ builtin_ffsll (bit)] - 1) ? Это потребовало бы некоторой простой работы для поиска по массиву, но, возможно, оно того стоит, если таблицы достаточно малы.

  3. Не вычисляйте f до тех пор, пока оно вам действительно не понадобится (ближе к концу, после вашего раннего выхода). Вы можете заменить код BASES_END на что-нибудь вроде


BASES_END:
ulong addToF = 0;
if (n > 1) {
    nu = minuu(nu, vals(n - 1));
    prod *= ugcd(nn, star(n));
    addToF = 1;
}
// ... early out if nu == 1...
// ... compute f ...
f += addToF;

Надеюсь, это поможет.

2
ответ дан 28 November 2019 в 05:13
поделиться

Пока непонятно, что вы ищете. Довольно часто теоретико-числовые задачи позволяют значительно ускорить работу за счет вывода математических свойств, которым должны удовлетворять решения.

Если вы действительно ищете целые числа, которые максимизируют количество не-свидетелей для теста MR (например, oeis.org/classic/A141768, о котором вы упоминаете), то можно использовать это количество не может быть больше, чем phi (n) / 4, и что целые числа, для которых имеется такое количество несвидетелей, являются произведением двух простых чисел вида

(k + 1) * (2k + 1)

или это числа Кармайкла с 3 простыми множителями.Я бы подумал, что сверх некоторого предела все целые числа в последовательности имеют эту форму и что это можно проверить, доказав верхнюю границу для свидетелей всех других целых чисел. Например. целые числа с 4 или более факторами всегда имеют не более phi (n) / 8 не-свидетелей. Аналогичные результаты могут быть получены из формулы количества оснований для других целых чисел.

Что касается микрооптимизации: всякий раз, когда вы знаете, что целое число делится на некоторое частное, тогда можно заменить деление умножением на обратное к частному по модулю 2 ^ 64. И тесты n% q == 0 могут быть заменены тестом

n * inverse_q

где inverse_q = q ^ (- 1) mod 2 ^ 64 и max_q = 2 ^ 64 / q. Очевидно, что для эффективности inverse_q и max_q необходимо предварительно вычислить, но, поскольку вы используете сито, я полагаю, это не должно быть препятствием.

5
ответ дан 28 November 2019 в 05:13
поделиться

Сначала немного придирок ;-) вам следует быть более внимательным к типам, которые вы используете. В некоторых местах вы, похоже, предполагаете, что ulong имеет ширину 64 бита, используйте uint64_t. А также для всех остальных типов тщательно продумайте, что вы от них ожидаете, и используйте соответствующий тип.

Оптимизация, которую я вижу, это целочисленное деление. Ваш код делает это очень часто, это, вероятно, самое дорогое, что вы делаете. Деление на маленькие целые числа (uint32_t) может быть гораздо эффективнее, чем на большие. В частности, для uint32_t существует инструкция ассемблера, которая выполняет деление и модуляцию за один раз, называется divl.

Если вы используете соответствующие типы, ваш компилятор может сделать все это за вас. Но вам лучше проверить ассемблер (опция -S в gcc), как уже кто-то сказал. Иначе легко включить некоторые небольшие фрагменты ассемблера здесь и там. Я нашел нечто подобное в своем коде:

register uint32_t a asm("eax") = 0;
register uint32_t ret asm("edx") = 0;
asm("divl %4"
    : "=a" (a), "=d" (ret)
    : "0" (a), "1" (ret), "rm" (divisor));

Как вы можете видеть, здесь используются специальные регистры eax и edx и тому подобное...

2
ответ дан 28 November 2019 в 05:13
поделиться
Другие вопросы по тегам:

Похожие вопросы: