Почему я вижу увеличение скорости ~ 20% при использовании нативного кода? - PullRequest
24 голосов
/ 19 мая 2009

Есть идеи, почему этот код:

extern "C" __declspec(dllexport) void Transform(double x[], double y[], int iterations, bool forward)
{
    long n, i, i1, j, k, i2, l, l1, l2;
    double c1, c2, tx, ty, t1, t2, u1, u2, z;

    /* Calculate the number of points */
    n = (long)pow((double)2, (double)iterations);

    /* Do the bit reversal */
    i2 = n >> 1;
    j = 0;
    for (i = 0; i < n - 1; ++i)
    {
        if (i < j)
        {
            tx = x[i];
            ty = y[i];
            x[i] = x[j];
            y[i] = y[j];
            x[j] = tx;
            y[j] = ty;
        }
        k = i2;
        while (k <= j)
        {
            j -= k;
            k >>= 1;
        }
        j += k;
    }

    /* Compute the FFT */
    c1 = -1.0; 
    c2 = 0.0;
    l2 = 1;
    for (l = 0; l < iterations; ++l)
    {
        l1 = l2;
        l2 <<= 1;
        u1 = 1; 
        u2 = 0;
        for (j = 0; j < l1; j++) 
        {
            for (i = j; i < n; i += l2) 
            {
                i1 = i + l1;
                t1 = u1 * x[i1] - u2 * y[i1];
                t2 = u1 * y[i1] + u2 * x[i1];
                x[i1] = x[i] - t1; 
                y[i1] = y[i] - t2;
                x[i] += t1;
                y[i] += t2;
            }
            z = u1 * c1 - u2 * c2;
            u2 = u1 * c2 + u2 * c1;
            u1 = z;
        }
        c2 = sqrt((1.0 - c1) / 2.0);
        if (forward) 
            c2 = -c2;
        c1 = sqrt((1.0 + c1) / 2.0);
    }

    /* Scaling for forward transform */
    if (forward)
    {
        for (i = 0; i < n; ++i)
        {
            x[i] /= n;
            y[i] /= n;
        }
    }
}

работает на 20% быстрее, чем этот код?

public static void Transform(DataSet data, Direction direction)
{
    double[] x = data.Real;
    double[] y = data.Imag;
    data.Direction = direction;
    data.ExtremeImag = 0.0;
    data.ExtremeReal = 0.0;
    data.IndexExtremeImag = 0;
    data.IndexExtremeReal = 0;

    long n, i, i1, j, k, i2, l, l1, l2;
    double c1, c2, tx, ty, t1, t2, u1, u2, z;

    /* Calculate the number of points */
    n = (long)Math.Pow(2, data.Iterations);

    /* Do the bit reversal */
    i2 = n >> 1;
    j = 0;
    for (i = 0; i < n - 1; ++i)
    {
        if (i < j)
        {
            tx = x[i];
            ty = y[i];
            x[i] = x[j];
            y[i] = y[j];
            x[j] = tx;
            y[j] = ty;
        }
        k = i2;
        while (k <= j)
        {
            j -= k;
            k >>= 1;
        }
        j += k;
    }

    /* Compute the FFT */
    c1 = -1.0; 
    c2 = 0.0;
    l2 = 1;
    for (l = 0; l < data.Iterations; ++l)
    {
        l1 = l2;
        l2 <<= 1;
        u1 = 1; 
        u2 = 0;
        for (j = 0; j < l1; j++) 
        {
            for (i = j; i < n; i += l2) 
            {
                i1 = i + l1;
                t1 = u1 * x[i1] - u2 * y[i1];
                t2 = u1 * y[i1] + u2 * x[i1];
                x[i1] = x[i] - t1; 
                y[i1] = y[i] - t2;
                x[i] += t1;
                y[i] += t2;
            }
            z = u1 * c1 - u2 * c2;
            u2 = u1 * c2 + u2 * c1;
            u1 = z;
        }
        c2 = Math.Sqrt((1.0 - c1) / 2.0);
        if (direction == Direction.Forward) 
            c2 = -c2;
        c1 = Math.Sqrt((1.0 + c1) / 2.0);
    }

    /* Scaling for forward transform */
    if (direction == Direction.Forward)
    {
        for (i = 0; i < n; ++i)
        {
            x[i] /= n;
            y[i] /= n;
            if (Math.Abs(x[i]) > data.ExtremeReal)
            {
                data.ExtremeReal = x[i];
                data.IndexExtremeReal = (int)i;
            }
            if (Math.Abs(y[i]) > data.ExtremeImag)
            {
                data.ExtremeImag = y[i];
                data.IndexExtremeImag = (int)i;
            }
        }
    }
}

БПФ http://www.rghware.com/fft.png

Я создал падение ЦП, видимое в середине графика, выбрав в своем приложении «NFT DLL FFT»:

http://www.rghware.com/InstrumentTuner.zip (исходный код)

Я думаю, что это будет работать на большинстве ПК. Вам нужно будет установить DirectX. У меня были некоторые проблемы с использованием настроек захвата для определенного оборудования. Предполагалось, что параметры захвата можно настраивать, но эта интересная находка застряла в стороне от разработки приложения.

В любом случае, почему я вижу увеличение скорости на 20% при использовании нативного кода? Кажется, это противоречит некоторым предположениям, которые у меня были ранее.

UPDATE

После преобразования функции в небезопасный метод и устранения проблемы long / int. Новый небезопасный метод на самом деле работает быстрее, чем собственный метод (довольно круто).

Профиль http://www.rghware.com/profile.png

Очевидно, что проверка привязки массива является причиной замедления на 20% в этом методе FFT. Из-за своей природы циклы for в этом методе не могут быть оптимизированы.

Спасибо всем за помощь.

Ответы [ 7 ]

23 голосов
/ 19 мая 2009

Просто глядя на этот код, я подозреваю, что из моего опыта довольно значительное замедление происходит от C ++ -> C #.

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

Кроме того, этот порт довольно близок к отображению 1-> 1 из C. Если вы запустите это через хороший .NET-профилировщик, вы, вероятно, найдете несколько отличных мест, которые можно оптимизировать, чтобы вернуть их обратно к Скорость C ++ с одной или двумя настройками (это почти всегда был мой опыт в портировании подпрограмм, подобных этой).

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


Редактировать: Я вижу еще одно огромное отличие, которое может быть причиной того, что ваш небезопасный код на C # работает медленнее.

Проверьте эту страницу о C # по сравнению с C ++ , в частности:

"Тип long: в C # тип long равен 64 битам, а в C ++ - 32 бита."

Вам следует преобразовать версию C #, чтобы использовать int, а не long. В C # long - это 64-битный тип. На самом деле это может оказать глубокое влияние на ваши манипуляции с указателями, потому что я считаю, что вы случайно добавляете конверсию long-> int (с проверкой переполнения) при каждом вызове указателя.

Кроме того, пока вы занимаетесь этим, вы можете попробовать обернуть всю функцию в непроверенный блок . C ++ не выполняет проверку переполнения, которую вы получаете в C #.

3 голосов
/ 19 мая 2009

Собственный компилятор может выполнять гораздо более глубокие и тяжелые оптимизации, чем JIT-компилятор, такие как векторизация, межпроцедурный анализ и т. Д. И FFT могут значительно ускорить процесс векторизации.

3 голосов
/ 19 мая 2009

Скорее всего, это связано с тем, что JIT-компилятор генерирует код, который не так эффективен, как код, сгенерированный собственным компилятором.

Профилирование кода, вероятно, должно стать вашим следующим шагом, если вас беспокоит снижение производительности на 20%, или вы можете подумать об использовании готовой оптимизированной библиотеки.

1 голос
/ 20 мая 2009

Я просто запустил код, который он выложил с int вместо long, и это не имело никакого значения. Я знаю, что другим людям повезло больше с FFT в .NET , показывая, что .NET может достигать или превосходить C ++ даже с математикой FFT.

Итак, мой ответ: либо код автора более оптимизирован (для C), чем код в ссылке, или он менее оптимизирован для C #, чем код в статье, на которую я ссылался.

Я выполнил два набора тестов на двух машинах с .NET 2.0. Одна машина имела XPSP2 и один процессор 850 МГц Pentium III с 512 МБ оперативной памяти. Другая машина была построена на 5321 Vista и имела один процессор, 2 ГГц Mobile Pentium 4, с 1 ГБ ОЗУ. В каждом случае я вычислял среднее из 100 отдельных расчетов БПФ на 217 (131072) значениях данных. Из этих значений я рассчитал стандартную ошибку из стандартного отклонения.

Результаты показаны в мс. Результаты для машины Pentium III:

  Not Optimized   Optimized For Space Optimized For Speed
Unmanaged   92.88 ± 0.09    88.23 ± 0.09    68.48 ± 0.03
Managed C++ 72.89 ± 0.03    72.26 ± 0.04    71.35 ± 0.06
C++/CLI 73.00 ± 0.05    72.32 ± 0.03    71.44 ± 0.04
C# Managed  72.21 ± 0.04    69.97 ± 0.08

Результаты для Mobile Pentium 4:

          Not Optimized   Optimized For Space Optimized For Speed
Unmanaged   45.2 ± 0.1  30.04 ± 0.04    23.06 ± 0.04
Managed C++ 23.5 ± 0.1  23.17 ± 0.08    23.36 ± 0.07
C++/CLI 23.5 ± 0.1  23.11 ± 0.07    23.80 ± 0.05
C# Managed  23.7 ± 0.1  22.78 ± 0.03
1 голос
/ 19 мая 2009

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

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

0 голосов
/ 13 июня 2009

Потому что компилятор C # .NET не самый лучший в создании эффективного кода. И вся логика языка мешает этому. Кстати, F # имеет гораздо лучшую производительность, чем C # в математике

0 голосов
/ 19 мая 2009

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

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