LINUX.ORG.RU

Микрооптимизация


0

2

У меня есть довольно большое математическое выражение которое транслируется в примерно такой сиплюсовый код:

TReal R1 = x * y;
TReal R2 = C1 * z;
TReal R3 = R1 + R2 + C2;
TReal R4 = exp(R3);
TReal R5 = pow(x, C3);
....
итд итп строк на 200-300

В примере R1 используется только один раз. Потому R1 можно запросто использовать еще раз. Но скажем R4 входит еще в несколько подвыражений идущих ниже. Так вот вопрос стоит ли мне заморачиватся с оптимизацией использования регистров или компилятор сам сообразит как куда что раскидать?

Так если грубо глянуть, просто по часам, icc будет побыстрее чем clang и vc, но совсем чуток, процентов на пять.

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

а теперь посмотри на его TReal, ты точно знаешь, что там у него не какой-то класс?

Не, не класс.

TReal double или float, ну или __m128d. Хотя если глянуть ассемблер там оно все через SSE делается, поэтому думаю что можно только базовые типы использовать.

ebantrop ()
Последнее исправление: ebantrop (всего исправлений: 1)
Ответ на: комментарий от ebantrop

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

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

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

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

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

конечно сообразит

Фига себе они умные. А скажем вот порядок оно тоже умеет? Например:

R1 = x + y + z;
R2 = sin(-x);
R3 = x * y * z;
...еще много чего...
R33 = R1 * R32 * R31;

Тут скажем R1 можно считать прям перед R33 и тут же его запользовать. Я такое на всякий случай сделал просто сортируя по глубине. А теперь вот думаю, мож компайлер сам лучше знает когда чего считать.

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

сравни код генерируемый без твоей оптимизации и с ней

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

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

техника оптимизации под линуха (ссылка старая, но почитать интересно. GCC 3.3.4: May 31, 2004)

Спасибо за ссыль. Многие штуки которые там описаны у меня сделаны. Например из такого:

sin(sqrt(2 + x^2)) + exp(sqrt(2 + x^2))

оно делает такое:

TReal R1 = x * x;
TReal R2 = 2 + R1;
TReal R3 = sqrt(R2);
TReal R4 = exp(R3);
TReal R5 = sin(R3);
TReal R6 = R4 + R5;

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

ebantrop ()

Эмм. Начни с профайлера. Всё, что до профайлера — не микрооптимизация, а преждевременная.

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

Эмм. Начни с профайлера. Всё, что до профайлера — не микрооптимизация, а преждевременная.

Уже. Там почти все время проводится. Загрузка данных — копейки.

ebantrop ()

А если сделать стековую машину которая будет это все считать, не получится что память будет очищаться автоматически?

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

[cite]

Например из такого:

sin(sqrt(2 + x^2)) + exp(sqrt(2 + x^2))

оно делает такое:

TReal R1 = x * x;
TReal R2 = 2 + R1;
TReal R3 = sqrt(R2);
[/cite] если у R1..Rx есть физический смысл, то это допустимо. Хотя когда прямое вычисление f(x) становится узким местом, его пытаются вычислить подругому - например разложить в ряд и запустить на сопроцессоре (на той-же видяхе) до требуемой точности или при итерациях можно считать f(x+1)-f(x),накапливать результат и учитывать погрешности; То есть делать оптимизации достойные человека, а не тупо пытаться разбросить выражение по регистрам конкретно-взятого процессора лучше компилятора.

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

А если сделать стековую машину которая будет это все считать, не получится что память будет очищаться автоматически?

Ну это я думаю компайлер кабэ должен сообразить. Что то же компайлер должен уметь делать.

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

...до требуемой точности или при итерациях можно считать f(x+1)-f(x),накапливать результат и учитывать погрешности;

Это выражение и есть итерации которые нужно считать. Все уже разложено, переложено, проинтегрировано и все что можно сделать аналитически сделать — сделано.

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

Чот ржу. Вы думаете компиляторы обезьяны пишут или решение оптимального использования ресурсов это задача ниже человеческого достоинства?

ebantrop ()

Было очень давно >20 лет назад, когда я занимался рассчетными задачами, то я компилировал код на с в ассемблер а, потом вручную его оптимизировал с учетом стека сопроцессора. Т.е. боролся за то, чтобы все промежуточные результаты жили в стеке. Получалось неплохо.

vromanov ★★ ()
$ ./run.sh
-1--------------------------------------------------
#include <stdio.h>
#include <math.h>
double fun1(double x)
{
  return sin(sqrt(2 + x*x)) + exp(sqrt(2 + x*x));
}
double fun2(double x)
{
  double R1 = x * x;
  double R2 = 2 + R1;
  double R3 = sqrt(R2);
  double R4 = exp(R3);
  double R5 = sin(R3);
  double R6 = R4 + R5;
  return R6;
}
double fun3(double x)
{
  double x2 = sqrt(2 + x*x);
  return sin(x2) + exp(x2);
}
int main()
{
  volatile double x = 0;
  printf("%f\n", fun1(x));
  printf("%f\n", fun2(x));
  printf("%f\n", fun3(x));
  return 0;
}-2--------------------------------------------------
#!/bin/bash

echo "-1--------------------------------------------------"
cat main.c
echo "-2--------------------------------------------------"
cat run.sh
echo "-3--------------------------------------------------"

rm -f out.*
rm -f *.dump
for key in O3 Ofast; do
  gcc main.c -$key -l m -o out.$key
  ./out.$key

  for((i=1;i<=3;i++)); do
    objdump -D out.$key |
    awk '/<fun'$i'>:/{f=1} {f=f&&($0!=""); if(f) print $0}' >fun$i.$key.dump
  done
  wc -l fun*.$key.dump | head -n 3
done
-3--------------------------------------------------
5.101016
5.101016
5.101016
  34 fun1.O3.dump
  23 fun2.O3.dump
  24 fun3.O3.dump
5.101016
5.101016
5.101016
  19 fun1.Ofast.dump
  19 fun2.Ofast.dump
  19 fun3.Ofast.dump
anonymous ()
Ответ на: комментарий от vromanov

...потом вручную его оптимизировал с учетом стека сопроцессора.

Вот этого как раз и хочется избежать.

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

То есть -Ofast вроде как все сам и должен сделать. Есть еще момент, что если выражение записать в виде fun1, то компилятор будет думать минуты. Clang просто валился, у gcc что-то получалось. icc честно говоря я отчаялся ждать где-то минут через 20 :(

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

читай про ключи оптимизации и пробуй с ними собираться. скажем, если пример описанный выше собрать с ключами -O2 и -ffast-math, то каждая из функций получиться длиной в 14 ассемблерных строк.

anonymous ()

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

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

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

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

читай про ключи оптимизации и пробуй с ними собираться. скажем, если пример описанный выше собрать с ключами -O2 и -ffast-math, то каждая из функций получиться длиной в 14 ассемблерных строк.

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

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

Да это более менее понятно что он делает. Вопрос насколько хорошо. Я на этапе трансляции в принципе делаю тоже самое что написано по ссылке, только с register allocation неохота связываться. Но это вроде как для компилятора не проблема.

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

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

Та не, должон влезть.

Если где-то можно сделать циклы то это может оказаться быстрее.

Код в принципе дергается из циклов где подсовываются данные. Мне кажется полный loop unrolling будет сильно жирно. И так работает вполне себе ничего. Хотя надо попробовать с инлайном и без.

ebantrop ()

boomerang

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

$ cat a.c
#include <stdio.h>
#include <math.h>

typedef float TReal;

int main()
{
        TReal x = rand();
        TReal y = rand();
        TReal z = rand();

        TReal C1 = 1;
        TReal C2 = 2;
        TReal C3 = 3;

        TReal R1 = x * y;
        TReal R2 = C1 * z;
        TReal R3 = R1 + R2 + C2;
        TReal R4 = exp(R3);
        TReal R5 = pow(x, C3);

        printf("R4=%f, R5=%f\n", R4, R5);
        return 0;
}
$ gcc a.c -lm -m32 -O2
$ boomerang ./a.out
$ cat output/a/crtstuff.c
// address: 0x8048470
int main(int argc, char **argv, char **envp) {
    long long local15;          // r24
    long long local18;          // r28
    long long local19;          // r32
    long long local20;          // r33
    int local4;                 // m[r28 - 20]
    long long local7;           // m[r28 - 40]

    rand();
    rand();
    rand();
    local19 = (float)local15 * local4;
    proc1();
    local19 = local4;
    pow(local19, *(int*)(local18 - 68));
    *(long long*)(local18 - 60) = local20;
    local19 = local7;
    *(long long*)(local18 - 68) = local19;
    printf("R4=%f, R5=%f\n", local19, 3.);
    return 0;
}
gv ()
Ответ на: комментарий от ebantrop

Хотя надо попробовать с инлайном и без.

-fpartial-inlining
Inline parts of functions. This option has any effect only when inlining itself is turned on by the -finline-functions or -finline-small-functions options.
Enabled at level -O2.

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

Что такие тулзы есть, это понятно. Особенно доставляют под .NET: восстанавливает все вплоть до имен переменных. Говорят Java не хуже, но я не копенгаген в ней. С нативным наверное проще ассемблер, да.

ebantrop ()

В примере R1 используется только один раз. Потому R1 можно запросто использовать еще раз.

На всякий случай укажи область действия переменной фигурными скобками

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

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

Это, пожалуй, самое оптимальное решение.

Смена версии компилятора тоже имеет значение. Порой очень неоднозначное.

Мне кажется полный loop unrolling будет сильно жирно.

Иногда хорошо помогает.

Хотя надо попробовать с инлайном и без.

LTO?

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

На всякий случай укажи область действия переменной фигурными скобками

Ненадо, комипайлеры сами разбираются.

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

Смена версии компилятора тоже имеет значение. Порой очень неоднозначное.

Неоднозначное это не то слово. Для TReal == double еще как то понятно. С sse и avx начинаются мистические преобразования кода. Оно конечно работает, но оч сильно зависит от того чем собрано. Icc порадовал.

Иногда хорошо помогает.

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

LTO?

В исполнении сумрачного интеловского гения это занимает вечность.

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

но оч сильно зависит от того чем собрано.

Пробовал компилировать свой код gcc-ям версий 4.6.3, 4.7.2 и 4.8.1. Как ни странно, но быстрее всех работает собранный 4.6.3.

Ну в каком то смысле я unrolling уже сделал на этапе трансляции.

Что под этим подразумевается? Вручную в смысле?

BTW, cуществует мнение, что работу по unrolling-у имеет смысл оставить компилятору. Мол, ручной unrolling только мешает компилятору понять как это соптимизировать наиболее эффективно. Тоже самое касаемо «массивы vs указатели».

В исполнении сумрачного интеловского гения это занимает вечность.

Это о времени компиляции? Все же имеет смысл попробовать. Я как-то видел ускорение почти в 3 раза (это скорее исключение, чем правило).

Есть еще PGO. В моем случае, правда, с чистым LTO результат получился лучше.

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

Пробовал компилировать свой код gcc-ям версий 4.6.3, 4.7.2 и 4.8.1. Как ни странно, но быстрее всех работает собранный 4.6.3.

Однако. А simd используется и там всякая векторизация?

Что под этим подразумевается? Вручную в смысле?

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

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

Однако. А simd используется и там всякая векторизация?

Не. Нет SIMD-а в target процессоре. Все целочисленное, часть кода переписана с использованием DSP-команд(saturation arithmetic) ARM-а на ассемблере.

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

=) башизм C-style - эдакий LOOP
пример: for (( var=0 ; var < 999 ; i++ )) ; do _command() ; done
где _command() - команда или функция
в ANSI C тоже что-то такое есть

ubuntuawp ★★ ()

Ежели кому еще интересно. С++ компиляторы офигенно умные. Интеловский немного похитрее будет, а так почти все работают очень и очень хорошо. У clang'a есть небольшой тупняк с векторизацией, но обещают поправить в следующем релизе.

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

компиляторы офигенно умные.

Я бы даже сказал «шибко» умные. Иногда приходится подсовывать данные ввода или rand()-а чтоб протестировать «скорострельность» чего-нибудь.

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