Как я могу улучшить / ускорить эту частую функцию в C? - PullRequest
3 голосов
/ 20 апреля 2010

Как я могу улучшить / ускорить эту частую функцию?

#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

#define M 10 // This is fixed
#define N 8  // This is NOT fixed

// Assumptions: 1. x, a, b and c are all arrays of 10 (M).
//              2. y and z are all matrices of 8 x 10 (N x M).
// Requirement: 1. return the value of ret;
//              2. get all elements of array c
float fnFrequentFunction(const float* x, const float* const* y, const float* const* z,
                         const float* a, const float* b, float *c, int n)
{
    register float tmp;
    register float sum;
    register float ret = 0;
    register const float* yy;
    register const float* zz;
    int i;

    for (i = 0; i < n; i++)  // M == 1, 2, 4, or 8
    {
        sum = 0;
        yy = y[i];
        zz = z[i];

        tmp = x[0] - yy[0]; sum += tmp * tmp * zz[0];
        tmp = x[1] - yy[1]; sum += tmp * tmp * zz[1];
        tmp = x[2] - yy[2]; sum += tmp * tmp * zz[2];
        tmp = x[3] - yy[3]; sum += tmp * tmp * zz[3];
        tmp = x[4] - yy[4]; sum += tmp * tmp * zz[4];
        tmp = x[5] - yy[5]; sum += tmp * tmp * zz[5];
        tmp = x[6] - yy[6]; sum += tmp * tmp * zz[6];
        tmp = x[7] - yy[7]; sum += tmp * tmp * zz[7];
        tmp = x[8] - yy[8]; sum += tmp * tmp * zz[8];
        tmp = x[9] - yy[9]; sum += tmp * tmp * zz[9];

        ret += (c[i] = log(a[i] * b[i]) + sum);
    }

    return ret;
}

// In the main function, all values are just example data.
int main()
{
    float x[M] = {0.001251f, 0.563585f, 0.193304f, 0.808741f, 0.585009f, 0.479873f, 0.350291f, 0.895962f, 0.622840f, 0.746605f};
    float* y[N];
    float* z[N];
    float a[M] = {0.870205f, 0.733879f, 0.711386f, 0.588244f, 0.484176f, 0.852962f, 0.168126f, 0.684286f, 0.072573f, 0.632160f};
    float b[M] = {0.871487f, 0.998108f, 0.798608f, 0.134831f, 0.576281f, 0.410779f, 0.402936f, 0.522935f, 0.623218f, 0.193030f};
    float c[N];

    float t1[M] = {0.864406f, 0.709006f, 0.091433f, 0.995727f, 0.227180f, 0.902585f, 0.659047f, 0.865627f, 0.846767f, 0.514359f};
    float t2[M] = {0.866817f, 0.581347f, 0.175542f, 0.620197f, 0.781823f, 0.778588f, 0.938688f, 0.721610f, 0.940214f, 0.811353f};
    int i, j;

    int n = 10000000;
    long start;

    // Initialize y, z for test example:
    for(i = 0; i < N; ++i)
    {
        y[i] = (float*)malloc(sizeof(float) * M);
        z[i] = (float*)malloc(sizeof(float) * M);

        for(j = 0; j < M; ++j)
        {
            y[i][j] = t1[j] * j;
            z[i][j] = t2[j] * j;
        }
    }


    // Speed test here:
    start = clock();
    while(--n)
        fnFrequentFunction(x, y, z, a, b, c, 8);
    printf("Time used: %ld\n", clock() - start);


    // Output the result here:
    printf("fnFrequentFunction == %f\n", fnFrequentFunction(x, y, z, a, b, c, 8));
    for(j = 0; j < N; ++j)
        printf("  c[%d] == %f\n", j, c[j]);
    printf("\n");


    // Free memory
    for(j = 0; j < N; ++j)
    {
        free(y[j]);
        free(z[j]);
    }

    return 0;
}

Любые предложения приветствуются: -)

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

Ответы [ 9 ]

13 голосов
/ 20 апреля 2010

поместите это вне цикла

sum = 0;

tmp = x[0] - y[0]; sum += tmp * tmp * z[0];
tmp = x[1] - y[1]; sum += tmp * tmp * z[1];
tmp = x[2] - y[2]; sum += tmp * tmp * z[2];
tmp = x[3] - y[3]; sum += tmp * tmp * z[3];
tmp = x[4] - y[4]; sum += tmp * tmp * z[4];
tmp = x[5] - y[5]; sum += tmp * tmp * z[5];
tmp = x[6] - y[6]; sum += tmp * tmp * z[6];
tmp = x[7] - y[7]; sum += tmp * tmp * z[7];
tmp = x[8] - y[8]; sum += tmp * tmp * z[8];
tmp = x[9] - y[9]; sum += tmp * tmp * z[9];
2 голосов
/ 20 апреля 2010
  • Эта функция идеально подходит для обработки SIMD. Посмотрите в документации вашего компилятора встроенные функции, которые соответствуют инструкциям SSE.
  • Вы можете разбить цепочку зависимостей по переменной sum. Вместо одного sum аккумулятора используйте два аккумулятора sum1 и sum2 поочередно - один для четных, другой для нечетных индексов. Добавьте их потом.
  • Единственное узкое место в производительности - функция log(). Проверьте, будет ли достаточно приближения. Вычисление этого также может быть векторизовано - я думаю, что Intel опубликовала высокопроизводительную математическую библиотеку - включая векторизованные версии функций, таких как log(). Вы можете использовать это.
  • Здесь вы работаете с float с, а log() использует точность double. Используйте logf() вместо этого. Это может (или не может) быть быстрее. Это, конечно, будет не медленнее.
  • Если ваш компилятор понимает C99, поместите квалификатор restrict в указатели, которые являются аргументами функции. Это говорит компилятору, что эти массивы не перекрываются, и может помочь ему генерировать более эффективный код.
  • Изменить способ хранения матриц в памяти. Вместо массива указателей, указывающих на непересекающиеся блоки памяти, используйте один массив размером M * N элементов.

Итак, чтобы сложить все вместе, вот как должна выглядеть функция. Это портативный C99. Используя встроенные в компилятор SIMD-функции, это можно сделать WAAAAY быстрее.

ОБНОВЛЕНИЕ: Обратите внимание, что я изменил способ определения входных матриц. Матрица - это один большой массив.

float fnFrequentFunction(const float *restrict x, const float *restrict y,
                         const float *restrict z, const float *restrict a,
                         const float *restrict b, float *restrict c, int n)
{
    float ret = 0;
    const float *restrict yy = y; //for readability
    const float *restrict zz = z; // -||-

    for (int i = 0; i < n; i++, yy += M, zz += M)  // n == 1, 2, 4, or 8
    {
        float sum = 0;
        float sum2 = 0;

        for(int j = 0; j < 10; j += 2)
        {
            float tmp  = x[j]   - yy[j];   sum  += tmp  * tmp  * zz[j];
            float tmp2 = x[j+1] - yy[j+1]; sum2 += tmp2 * tmp2 * zz[j+1];
        }
        sum += sum2;

        ret += (c[i] = logf(a[i] * b[i]) + sum);
    }
    return ret;
}
1 голос
/ 20 апреля 2010

Используйте памятка для кэширования результатов. Это оптимизация компромисса между временем и пространством.

Это действительно легко сделать в Perl с пакетом memoize и, возможно, во многих других динамических языках. В Си вам нужно бросить свой собственный.

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

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

0 голосов
/ 20 апреля 2010

Это, кажется, расчет модели гауссовой смеси.Несколько лет назад я работал над оптимизацией этого алгоритма, который использовался как часть программы обработки речи.Я исследовал ряд оптимизаций, которые вы пытаетесь сделать, но никогда не находил ничего, используя прямую C, чтобы получить больше, чем несколько процентов.Мой самый большой выигрыш пришел от перекодирования базового ядра GMM с использованием инструкций SIMD.Поскольку это все еще не обеспечивало производительность, которую я искал, следующим (и последним) шагом было использование графического процессора Nvidia.Это сработало, но программирование этой вещи само по себе было головной болью.

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

0 голосов
/ 20 апреля 2010

Если вы вызываете это несколько раз, когда a и b не изменились, то объедините a и b в logab, где logab [i] = log (a [i] * b [i]), так как a и b не используются где-нибудь еще.

0 голосов
/ 20 апреля 2010

В дополнение к ответу Андрея, вы можете добавить в цикл некоторую предварительную выборку:

float fnFrequentFunction(const float* x, const float* y, const float* z,
                         const float *a, const float *b, float *c, int M)
{
    register float tmp;
    register float sum;
    register float ret = 0;
    int i;
    sum = 0;

    tmp = x[0] - y[0]; sum += tmp * tmp * z[0];
    tmp = x[1] - y[1]; sum += tmp * tmp * z[1];
    tmp = x[2] - y[2]; sum += tmp * tmp * z[2];
    tmp = x[3] - y[3]; sum += tmp * tmp * z[3];
    tmp = x[4] - y[4]; sum += tmp * tmp * z[4];
    tmp = x[5] - y[5]; sum += tmp * tmp * z[5];
    tmp = x[6] - y[6]; sum += tmp * tmp * z[6];
    tmp = x[7] - y[7]; sum += tmp * tmp * z[7];
    tmp = x[8] - y[8]; sum += tmp * tmp * z[8];
    tmp = x[9] - y[9]; sum += tmp * tmp * z[9];

    for (i = 0; i < M; i++)  // M == 1, 2, 4, or 8
    {
        //----------------------------------------
        // Prefetch data into the processor's cache
        //----------------------------------------
        float a_value = a[i];
        float b_value = b[i];
        float c_value = 0.0;

        //----------------------------------------
        // Calculate using prefetched data.
        //----------------------------------------
        c_value = log(a_value * b_value) + sum;
        c[i] = c_value;
        ret += c_value;
    }

    return ret;
}

Вы также можете попробовать развернуть цикл:

float a_value = 0.0;
float b_value = 0.0;
float c_value = 0.0;
--M;
switch (M)
{
    case 7:
        a_value = a[M];
        b_value = b[M];
        c_value = log(a_value * b_value) + sum;
        c[M] = c_value;
    ret += c_value;
    --M;
    case 6:
        a_value = a[M];
        b_value = b[M];
        c_value = log(a_value * b_value) + sum;
        c[M] = c_value;
    ret += c_value;
    --M;
    case 5:
        a_value = a[M];
        b_value = b[M];
        c_value = log(a_value * b_value) + sum;
        c[M] = c_value;
    ret += c_value;
    --M;
    case 4:
        a_value = a[M];
        b_value = b[M];
        c_value = log(a_value * b_value) + sum;
        c[M] = c_value;
    ret += c_value;
    --M;
    case 3:
        a_value = a[M];
        b_value = b[M];
        c_value = log(a_value * b_value) + sum;
        c[M] = c_value;
    ret += c_value;
    --M;
    case 2:
        a_value = a[M];
        b_value = b[M];
        c_value = log(a_value * b_value) + sum;
        c[M] = c_value;
    ret += c_value;
    --M;
    case 1:
        a_value = a[M];
        b_value = b[M];
        c_value = log(a_value * b_value) + sum;
        c[M] = c_value;
    ret += c_value;
    --M;
    case 0:
        a_value = a[M];
        b_value = b[M];
        c_value = log(a_value * b_value) + sum;
        c[M] = c_value;
    ret += c_value;
    break;
}

Глядя на развернутую версию, вы можете вынуть "+ sum" из "loop" и добавить ее в конце как: ret += (M + 1) * sum; поскольку sum не меняется.

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

float product[8];
for (i = 0; i < M; ++i)
{
  product[i] = a[i] * b[i];
}
for (i = 0; i < M; ++i)
{
  c[i] = log(product);
  ret += c[i];
}
ret += M * sum;
0 голосов
/ 20 апреля 2010

Интересно, может ли это ускорить процесс с использованием масштабированных целых, а не с плавающей точкой? Я не знаю диапазоны данных, поэтому я не знаю, возможно ли это вообще

0 голосов
/ 20 апреля 2010

Вы используете оптимизацию компилятора?

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

Time used: 8720000

и без регистровых чисел:

Time used: 8710000

Я знаю, что это немного.

Я предполагаю, что вы сделали все эти суммы, чтобы избежать цикла for, потому что вы думаете, что это намного медленнее. Это не. Современный компилятор сделает эту оптимизацию и для вас.

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

0 голосов
/ 20 апреля 2010

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

Помимо этого, вы начинаете переходить к точке, в которой вы хотите знать, на какой тип процессора вы нацелены. Если у него есть собственная поддержка SIMD, FPU, какой тип кэша он использует и т. Д. Также в зависимости от того, сколько аргументов передается через регистры, сокращение параметров путем объединения в одну структуру и передачи по ссылке может дать вам небольшой прирост. Объявление переменных в качестве регистра может помочь, а может и не помочь. Снова профилирование и проверка результатов ассемблера ответит на это.

Поскольку сумма известна до цикла, вы можете избежать добавления значения M * после цикла для повышения. Это просто оставляет 2 лога на внутренней стороне.

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

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

Всего несколько мыслей.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...