LINUX.ORG.RU

Оптимизация вычислений на крестах


0

2

Я уже в торсионном треде спрашивал @AntonI про числодробилки на плюсах, но возможно кто-нибудь ещё сможет дать совет по тому, как правильно оптимизировать вычисления на плюсах (фортран тоже сойдёт на самом деле, но нужны веские аргументы, как впрочем и питон с нумпаями).

Я вычисляю значение комплекснозначной функции от трёх действительных аргументов (плюс ещё порядка тридцати действительных параметров, которые при вычислении фиксированы на каждую итерацию фита). Пока я взял фортрановский код, соответствующий данной статье, и переписал его в виде класса с методами const и double. Пример ниже приведён. Таких функций я должен буду насчитывать по миллиарду раз на только одну итерацию фита, поэтому и встаёт вопрос об оптимальном написании кода.

Заголовочный файл вот такой:

#include <complex>
typedef std::complex<double> complex;
class ff_t
{
    private:
        double mro, gro, mrp, grp, mf2, gf2, mf0, gf0, msg, gsg;
        complex bt1, bt2, bt3, bt4, bt5, bt6, bt7;
        double mpi, mpi2;
        double sqr (double x) const {return x*x;}
    public:
        ff_t ();
        complex xi1 (double qq, double s1, double s2) const;
};

Кусок файла с кодом:

#include "ff.hpp"
#include <cmath>

complex ff_t::bw (double s, double m, double g, int l) const
{
    const auto mp = 4*sqr (mpi);
    const auto msq = sqr (m);
    const auto w = std::sqrt (s);
    double wgs = 0.0;
    if (w>2*mpi) {
        auto qs = std::sqrt (std::abs ((s-mp)*s))/w;
        auto qm = std::sqrt (std::abs ((msq-mp)*msq))/m;
        const int ipow = 2*l+1;
        wgs = g*(msq/w)*std::pow(qs/qm, ipow);
    }
    return complex (msq, 0.)/ complex (msq-s, -wgs);
}

complex ff_t::xi1 (double qq, double s1, double s2) const
{
    // check phase space
    auto s3 = qq - s1 - s2 + 3*mpi2;
    if ((s3 <= 0.) or (s2 <= 0.))
        return complex(0., 0.);
    const auto f134 = -1./3. * ((s3-mpi2)-(s1-mpi2));
    const auto f15a = -1./2. * ((s2-mpi2)-(s3-mpi2));
    const auto f15b = -1./18.* (qq-mpi2+s2)*(4.*mpi2-s2)/s2;
    const auto f167 = -2./3.;

    const auto fro1 = bw (s1, mro, gro/*o*/, 1);
    const auto frp1 = bw (s1, mrp, grp, 1);
    const auto fro2 = bw (s2, mro, gro, 1);
    const auto frp2 = bw (s2, mrp, grp, 1);
    const auto ff21 = bw (s1, mf2, gf2, 2);
    const auto ff22 = bw (s2, mf2, gf2, 2);
    const auto fsg2 = bw (s2, msg, gsg, 0);
    const auto ff02 = bw (s2, mf0, gf0, 0);

    return
         bt1*fro1      + bt2*frp1+
         bt3*f134*fro2 + bt4*f134*frp2
       - bt5*f15a*ff21 - bt5*f15b*ff22
       - bt6*f167*fsg2 - bt7*f167*ff02;
}

И флаги компиляции:

CFLAGS=-pthread -std=c++14 -m64 -fext-numeric-literals
CFLAGS += -Wall -Werror -Wpacked -malign-double -O3\
          -mpreferred-stack-boundary=8 -Wfatal-errors
★★★★★
Ответ на: комментарий от AntonI

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

luke ★★★★★
() автор топика

Примерно как-то так https://godbolt.org/z/xb4Yco

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

Siborgium ★★★★★
()
Последнее исправление: Siborgium (всего исправлений: 2)
Ответ на: комментарий от luke

Ну по коду - убрать дублирующиеся вызовы спецфункций. Скажем там несколько раз корень из s1 и s2 считается, наверное есть еще что то.

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

По алгоритму - тут Вам виднее, наверное для фиттинга надо считать якобианы, и лучше делать это аналитически. Тяжелый счёт лучше запараллелить - но это мб модуль который фиттинг делает сам может?

AntonI ★★★★★
()
Ответ на: комментарий от Siborgium

Спасибо! Буду думать дальше. А есть что почитать на эту тему именно для современных x86, хотя общая теория тоже зайдёт.

luke ★★★★★
() автор топика
Ответ на: комментарий от AntonI

Ну по коду - убрать дублирующиеся вызовы спецфункций

Компилятор разве такое не должен разворачивать, если аргумент не меняется?

наверное для фиттинга надо считать якобианы

Я ещё не начинал, но скорее всего понадобится первая и мб вторая производные по параметрам. Впрочем, минуит вроде бы умел работать и так, посмотрим.

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

luke ★★★★★
() автор топика
Ответ на: комментарий от Siborgium

А ещё вот вопрос: если размер массива начнёт расти с 8 до, скажем, 30, имеет смысл продолжать все эти однотипные переменные складывать в один C++ array?

luke ★★★★★
() автор топика
Ответ на: комментарий от luke

Компилятор разве такое не должен разворачивать, если аргумент не меняется?

Гляньте че там выхлоп с -S выдает. Может и развернет, может и нет.

понадобится первая и мб вторая производные по параметрам. Впрочем, минуит вроде бы умел работать и так

Да, но это будет медленнее скорее всего.

AntonI ★★★★★
()
Ответ на: комментарий от fsb4000

Два чая этому господину. М.б. там вообще все считается за десятки минут и нефиг париться;-)

AntonI ★★★★★
()
Ответ на: комментарий от fsb4000

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

zerhud
()

Ещё между -O3 и -Ofast разница в ассемблере в 2 раза :)

https://godbolt.org/z/rdYoTo

Но понятно, что это не замер времени, и нужны тесты, чтобы посмотреть, что при -Ofast вычисления остались корректными.

fsb4000 ★★★★★
()
Ответ на: комментарий от fsb4000

Я замерял кстати таймом, но там вычислялась и фортрановская функция, и моя, чисто для того чтобы проверить, считаю ли я то же самое, что и фортран. Там было на моем x220 три секунды на сто тыщ событий в один поток.

Пардон за анонимуса,

Люк

anonymous
()
Ответ на: комментарий от Siborgium

Ещё можно вызов pow развернуть, ибо там только первая, третья, и пятая степени используются.

luke ★★★★★
() автор топика
Ответ на: комментарий от luke

Да, конечно, пока количество переменных – фиксированная величина. Смысл не в складывании в массив, смысл в том, чтобы анролл не писать руками, а свалить на компилятор.

По коду: в формировании indices есть косяк, там должно быть indices[len] = i, а не indices[i] = i. Кроме того, с утра подзадумался, есть ли вообще в этом смысл – если переменных всего 8, быстрее посчитать wgs для всех, а потом обнулить не удовлетворяющие условию. Так, по крайней мере, получится нормально разложить цикл на несколько циклов все для того же нормального анролла. С indices так не получится.

Про степени верное замечание, согласен.

Siborgium ★★★★★
()

а на видиакартачке нельзя чтоли такое посчитать? сразу x100 ускарение будит

anonymous
()
Ответ на: комментарий от anonymous

В том и дело что не проверял. Поэтому таких выводов не делал.

deep-purple ★★★★★
()
Ответ на: комментарий от Siborgium

Естественно, там же восемь call’ов

luke ★★★★★
() автор топика
Ответ на: комментарий от Siborgium

если переменных всего 8

потом может быть больше.

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

luke ★★★★★
() автор топика
Ответ на: комментарий от deep-purple

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

Но само вычисление должно быть оптимальным.

А потом надо будет ещё сумму насчитывать…

luke ★★★★★
() автор топика
Ответ на: комментарий от anonymous

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

А кластера видеокарточек у нас нет.

luke ★★★★★
() автор топика
Ответ на: комментарий от luke

Условие проверить можно, и даже нужно. Тем не менее, ключом здесь выступает ветвление, которое в идеале нужно устранить.

Siborgium ★★★★★
()
Ответ на: комментарий от luke

Никакой разницы нет, только с for_each не получится использовать связывать по i массивы.

Siborgium ★★★★★
()
Ответ на: комментарий от luke

Его дешево проверять, но ветвление на основе этого условия сломает векторизацию. Если бы не sqrt, я бы на 100% был бы уверен, что вторая версия будет в разы быстрее – нет принципиальной разницы между расчетом 3 значений и 8, это займет одинаковое количество времени, если это векторизовано.

Siborgium ★★★★★
()
Последнее исправление: Siborgium (всего исправлений: 2)
Ответ на: комментарий от anonymous

Tам солянка из старых зионов и оптеронов, всего где-то 500 ядер.

А флопсы я не измерял.

luke ★★★★★
() автор топика
Ответ на: комментарий от luke

а архитектура минимальная какая? avx-то хоть есть? а то может вы зря тут тужитесь. ещё, вот, вы в compiler explorer ставите -march=native, но это же неправильно, там даже предупреждение выскакивает.

попробуй добавить флаги -ffast-math -fassociative-math.

anonymous
()
Ответ на: комментарий от anonymous

Sandy Bridge есть, мои примеры с -msandybridge.

попробуй добавить флаги

Да вряд ли уже получится быстрее после переписывания, но если будут условные переходы в ассемблере то попробуем и их.

luke ★★★★★
() автор топика
Ответ на: комментарий от luke
    const auto msq = sqr (m);
    const auto w = std::sqrt (s);
    double wgs = 0.0;
    if (w>2*mpi) {
        auto qs = std::sqrt (std::abs ((s-mp)*s))/w;
        auto qm = std::sqrt (std::abs ((msq-mp)*msq))/m;
        const int ipow = 2*l+1;
        wgs = g*(msq/w)*std::pow(qs/qm, ipow);
    }

можно заменить на

    const auto msq = sqr (m);
    const auto w = std::sqrt (s);
    double wgs = 0.0;
    if (w>2*mpi) {
        double wtf = std::sqrt (std::abs ((s-mp)/(msq-mp)) );
        const int ipow = 2*l+1;
        wgs = g*(msq/w)*std::pow(wtf, ipow);
    }

да, ведь?

anonymous
()
Ответ на: комментарий от anonymous

попробуй добавить флаги -ffast-math

С fast-math нужно быть очень и очень аккуратным: работает правильно только если nan и/или +/-inf никогда не случаются.

bugfixer ★★★★★
()
Ответ на: комментарий от Siborgium

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

ладно, давай зачётку :)

anonymous
()
Ответ на: комментарий от anonymous

Неправда ваша, вот код ТС.

auto qs = std::sqrt (std::abs ((s-mp)*s))/w;
auto qm = std::sqrt (std::abs ((msq-mp)*msq))/m;
const int ipow = 2*l+1;
wgs = g*(msq/w)*std::pow(qs/qm, ipow);
Siborgium ★★★★★
()
Ответ на: комментарий от Siborgium

я тут вижу, что в степень возводится частное от корней (или корень от частного), потом то, что возвели в степень умножается на g*(msq/w).

а у вас я вижу

    for (unsigned i = 0; i < SIZE; i++) {
        qss_qms[i] = qss[i] / qms[i];
        qss_qms[i] = std::abs(qss_qms[i]);
        qss_qms[i] = std::sqrt(qss_qms[i]);
        qss_qms[i] *= ms[i];
        qss_qms[i] /= ws[i];
    }
    for (unsigned i = 0; i < 3; i++) { 
        qss_qms[i] *= qss_qms[i] * qss_qms[i];
    }
    for (unsigned i = 3; i < 5; i++) {
        double a = qss_qms[i];
        qss_qms[i] *= qss_qms[i];
        qss_qms[i] *= qss_qms[i];
        qss_qms[i] *= a;
    }

сначала (корень*умножение/деление), а потом возведение всего этого дела в n-ную степень. но я сегодня очки забыл, поэтому могу и ошибаться.

anonymous
()
Ответ на: комментарий от anonymous

парсер косячит, когда выбираешь С-синтаксис.

for (unsigned i = 0; i < SIZE; i++) {
    qss_qms[i] = qss[i] / qms[i];
    qss_qms[i] = std::abs(qss_qms[i]);
    qss_qms[i] = std::sqrt(qss_qms[i]);
    qss_qms[i] *= ms[i];
    qss_qms[i] /= ws[i];
}
for (unsigned i = 0; i < 3; i++) { 
    qss_qms[i] *= qss_qms[i] * qss_qms[i];
}
for (unsigned i = 3; i < 5; i++) {
    double a = qss_qms[i];
    qss_qms[i] *= qss_qms[i];
    qss_qms[i] *= qss_qms[i];
    qss_qms[i] *= a;
}
anonymous
()
Ответ на: комментарий от anonymous

Первый цикл – (корень*умножение/деление). Остальные – возведение в степень.

Что не так?

Siborgium ★★★★★
()
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.