Ужасная производительность - простая проблема накладных расходов или программный недостаток?

У меня есть то, что я понимаю как относительно простую конструкцию OpenMP. Проблема в том, что программа работает примерно в 100-300 раз быстрее с 1 потоком по сравнению с 2 потоками. 87% программы тратится на gomp_send_wait() и еще 9,5% на gomp_send_post.

Программа дает правильные результаты, но мне интересно, есть ли ошибка в коде, которая вызывает некоторый конфликт ресурсов, или просто накладные расходы на создание потока совершенно не стоят того для цикла размером фрагмента 4. p варьируется от 17 до 1000, в зависимости от размера моделируемой молекулы.

Мои цифры приведены для наихудшего случая, когда p равно 17, а размер блока равен 4. Производительность одинакова, независимо от того, использую ли я статическое, динамическое или управляемое планирование. С p=150 и размером фрагмента 75 программа по-прежнему в 75-100 раз медленнее, чем последовательная.

...
    double e_t_sum=0.0;
    double e_in_sum=0.0;

    int nthreads,tid;

    #pragma omp parallel for schedule(static, 4) reduction(+ : e_t_sum, e_in_sum) shared(ee_t) private(tid, i, d_x, d_y, d_z, rr,) firstprivate( V_in, t_x, t_y, t_z) lastprivate(nthreads)
    for (i = 0; i < p; i++){
        if (i != c){
            nthreads = omp_get_num_threads();               
            tid = omp_get_thread_num();

            d_x = V_in[i].x - t_x; 
            d_y = V_in[i].y - t_y;
            d_z = V_in[i].z - t_z;


            rr = d_x * d_x + d_y * d_y + d_z * d_z;

            if (i < c){

                ee_t[i][c] = energy(rr, V_in[i].q, V_in[c].q, V_in[i].s, V_in[c].s);
                e_t_sum += ee_t[i][c]; 
                e_in_sum += ee_in[i][c];    
            }
            else{

                ee_t[c][i] = energy(rr, V_in[i].q, V_in[c].q, V_in[i].s, V_in[c].s);
                e_t_sum += ee_t[c][i]; 
                e_in_sum += ee_in[c][i];    
            }

            // if(pid==0){printf("e_t_sum[%d]: %f\n", tid, e_t_sum[tid]);}

        }
    }//end parallel for 


        e_t += e_t_sum;
        e_t -= e_in_sum;            

...

person Community    schedule 15.05.2009    source источник
comment
Сколько процессоров в системе, на которой вы работаете?   -  person Michael    schedule 16.05.2009


Ответы (6)


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

ИМО есть три возможных объяснения замедления:

  1. Это может легко объяснить замедление. Элементы массива ee_t ведут к ложному совместному использованию внутри строки кэша. Ложное совместное использование — это когда ядра в конечном итоге записывают в одну и ту же строку кеша не потому, что они на самом деле обмениваются данными, а когда то, что записывают ядра, просто оказывается в той же строке кеша (поэтому это называется false). обмен). Я могу объяснить больше, если вы не найдете ложный обмен в Google. Выравнивание строки кэша элементов ee_t может очень помочь.

  2. Накладные расходы на порождение выше, чем преимущества параллелизма. Вы пробовали менее 8 ядер? Как производительность на 2 ядрах?

  3. Общее количество итераций невелико, скажем, возьмем 17 в качестве примера. Если вы разделите его на 8 ядер, у него будут проблемы с дисбалансом нагрузки (тем более, что часть ваших итераций практически не выполняет никакой работы (когда i == c). Как минимум одно ядро ​​должно будет сделать 3 итерации, а все остальные будут сделать 2. Это не объясняет замедление, но, безусловно, является одной из причин, почему ускорение не так высоко, как вы можете ожидать.Поскольку ваши итерации имеют разную длину, я бы использовал динамическое расписание с размером фрагмента 1 или использовал руководство openmp. Поэкспериментируйте с размером чанка, слишком маленький чанк также приведет к замедлению.

Дайте мне знать, как это происходит.

person Aater Suleman    schedule 10.05.2011

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

В любой книге по оптимизации последовательного кода есть правило номер 1 для циклов: удалить все условные операции. Стоимость тестов. Некоторые компиляторы (кстати, вы никогда не говорите, какую ОС/компилятор/процессор вы используете... это имеет значение) могут попытаться оптимизировать условный код. Некоторые компиляторы (например, компилятор Sun C) даже позволяют вам запускать программу в режиме «сбора», где она генерирует информацию о профиле времени выполнения о том, как часто выполняются ветви условного оператора, а затем позволяют перекомпилировать в режиме, который использует эти собранные данные. для оптимизации сгенерированного кода. (См. параметр -xprofile)

Первое правило оптимизации параллельного кода: сначала выполните максимально возможную последовательную оптимизацию. Затем запараллелить петли.

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

Тем не менее, результаты могут различаться в зависимости от ОС/компилятора/платформы.

См. Использование OpenMP< /em> и Программирование приложений для Solaris

person Community    schedule 17.05.2009
comment
Опять же, вы твердите, по общему признанию, о плохой, но не относящейся к делу проблеме с кодом. Как оказалось, я написал тесты вне цикла сразу после поста Meitu. Естественно, он на 15% быстрее последовательного и, естественно, имеет те же проблемы с относительной производительностью, что и при использовании OpenMP. Где я утверждал, что весь параллельный код работает быстрее? Я специально спросил в этом случае, потому что производительность была намного хуже, и в прошлом я видел, что это происходило из-за неправильного кода. Теперь, когда дохлая лошадь достаточно избита, возможно, кто-нибудь еще придет сюда и ответит на мой вопрос. - person ; 17.05.2009

Я считаю, что вам следует попробовать переместить все эти ветки (т.е. если) внутри цикла и сделать это в двух отдельных циклах, один для i ‹ c и один для i > c. Это принесет большую пользу даже однопоточному коду, но должно дать вам больше параллелизма, даже если, как вы сказали, накладные расходы на создание потока могут быть больше, чем преимущества для малых n.

person Metiu    schedule 15.05.2009
comment
Спасибо за эту запись. Я думаю, вы абсолютно правы, что это улучшит код, чтобы вывести эти два теста из внутреннего цикла, создав два цикла. Я обязательно сделаю это сегодня. Однако босс хочет увидеть версию OpenMP, и его не удовлетворит простое удаление некоторых тестов внутреннего цикла. :) - person ; 16.05.2009

Метиу прав. Вы не можете ожидать хорошей производительности от цикла, в котором есть условные операторы. Это просто плохое кодирование. Даже для скалярной производительности.

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

Тесты удалять не нужно. Цикл необходимо перестроить. И скалярная производительность тоже выиграет.

person Community    schedule 16.05.2009
comment
В своем исходном посте я провел относительное сравнение производительности кода с одним потоком и с несколькими потоками. Поскольку плохое относительно, я думаю, будет справедливо сказать, что производительность многопоточного исполнения плохая по сравнению с последовательной версией. Я действительно считаю, что примеры улучшений кода, которые вы привели, ортогональны проблеме производительности OpenMP, но если вы хотите объяснить, почему это мнение ошибочно, я был бы рад узнать что-то новое. Возможно, вы можете объяснить, что вы подразумеваете под циклом, который необходимо реструктурировать - каким образом? - person ; 17.05.2009

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

Еще одна большая возможность: реализация сокращения в GOMP может быть очень плохой (на это указывают данные вашего профиля), и он генерирует сообщения после каждого блока, а не накапливает в каждом потоке, а затем собирает их в конце. Попробуйте выделить e_t_sum и e_in_sum как массивы с nthreads элементами в каждом и добавить к e_t_sum[tid] внутри цикла, а затем перебрать их, чтобы вычислить глобальную сумму после завершения параллельного цикла.

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

Другая возможность: вы можете столкнуться с ложным обменом обновлениями элементов ee_t. Обеспечьте выравнивание этого массива и попробуйте размеры фрагментов, кратные размеру строки кэша. Одним тонким намеком на эту патологию может быть часть петли, где i > c занимает непропорционально больше времени, чем часть, где i < c.

person Phil Miller    schedule 18.06.2009

Это похоже на проблему с реализацией openmp компилятора GNU. Попробуйте другой компилятор. У Intel есть компилятор Linux, копию которого вы можете скачать и попробовать здесь.

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

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

person Community    schedule 22.05.2009