Почему я получаю большее относительное ускорение по сравнению со скаляром от встроенных SIMD с большими массивами? - PullRequest
2 голосов
/ 12 января 2020

Я хочу научиться программированию SIMD. Теперь у меня есть интересный момент в моем коде.

Я просто хочу измерить время работы моего кода. Я пытаюсь применить некоторые базовые функции для моего массива с определенным размером.

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

Я определил производительность как (время без sse) / (время с использованием sse).

Но когда у меня размер 8, у меня производительность составляет 1,3, а когда мой размер = 512 - у меня производительность = 3, если у меня размер = 1000, производительность = 4, если размер = 4000 -> производительность = 5.

Я не понимаю, почему моя производительность увеличивается при увеличении размера массива.

Мой код

void init(double* v, size_t size) {
    for (int i = 0; i < size; ++i) {
        v[i] = i / 10.0;
    }
}

void sub_func_sse(double* v, int start_idx) {
    __m256d vector = _mm256_loadu_pd(v + start_idx);
    __m256d base = _mm256_set_pd(2.0, 2.0, 2.0, 2.0);
    for (int i = 0; i < 128; ++i) {
        vector = _mm256_mul_pd(vector, base);
    }
    _mm256_storeu_pd(v + start_idx, vector);
}

void sub_func(double& item) {
    for (int k = 0; k < 128; ++k) {
        item *= 2.0;
    }
}

int main() {
    const size_t size = 8;
    double* v = new double[size];
    init(v, size);
    const int num_repeat = 2000;//I should repeat my measuraments 
                               //because I want to get average time - it is more clear information
    double total_time_sse = 0;
    for (int p = 0; p < num_repeat; ++p) {
        init(v, size);
        TimerHc t;
        t.restart();
        for (int i = 0; i < size; i += 8) {
            sub_func_sse(v, i);
        }
        total_time_sse += t.toc();
    }

    double total_time = 0;
    for (int p = 0; p < num_repeat; ++p) {
        init(v, size);
        TimerHc t;
        t.restart();
        for (int i = 0; i < size; ++i) {
            sub_func(v[i]);
        }
        total_time += t.toc();
    }
    std::cout << "time using sse = " << total_time_sse / num_repeat << std::endl <<
        "time without sse = " << total_time / num_repeat << std::endl;
    system("pause");
}

Ответы [ 2 ]

3 голосов
/ 12 января 2020

Я определил производительность как (время без sse) / (время с использованием sse).

То, что вы измеряете, - это ускорение.

Ускорение, которое вы можете ожидать от применения распараллеливания, моделируется законом Амдаля . Это связывает экономию в тех частях, которые могут быть сделаны быстрее (распараллеливанием или другими средствами) с общим ускорением. Закон Амдала может быть довольно пугающим, потому что он в основном гласит, что ускорение деталей не всегда даст вам полное ускорение. Предел достижимого ускорения определяется относительной долей рабочей нагрузки, которую можно распараллелить.

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

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

PS: Это всего лишь несколько движений, чтобы объяснить, почему эффект, который вы видите, не слишком удивителен. К счастью, есть еще один ответ, который более подробно описывает ваш конкретный c тест.

1 голос
/ 12 января 2020

Вы, вероятно, жертва масштабирования частоты процессора ; для стабильных результатов вы должны отключить Dynami c частотное масштабирование и Turbo Boost , или, по крайней мере, прогреть процессор перед началом измерения.

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

Сказав это, у вашего подхода есть несколько других проблем:

  • Издержки high_frequency_clock::now() вызовов по сравнению с измеряемой работой высоки; переместите измерение времени за пределы for (..num_repeat l oop, то есть время на все l oop, а не на отдельные итерации (затем при желании разделите измеренное время на количество итераций).

  • Результаты вычислений никогда не используются; компилятор может полностью оптимизировать работу. Обязательно «используйте» результат, например, напечатав его.

  • Умножить двойное на 2.0 весьма неэффективно. Действительно, версия без SSE оптимизирована для ADD (item *= 2.0 ==> vaddsd xmm0, xmm0, xmm0). Так что ваша версия SSE, созданная вручную, проигрывает.

  • Оптимизирующий компилятор, вероятно, автоматически векторизует ваш код не-SSE. Чтобы быть уверенным, всегда проверяйте сгенерированную сборку. Ссылка на Godbolt

  • Используйте систему сравнения, такую ​​как Google Benchmark ; это поможет вам избежать многих ловушек, связанных с тестированием кода.

...