Распространенная ситуация при программинге, как быть? Быстродействие vs Лаконичность - страница 2

 

Странно, что никто не обратил внимание, но...

    for (int Pos = 0; Pos < AMOUNT; Pos++)
    {
      Num1 = Source1[Pos].bid;
      Num2 = Source1[Pos].ask;
      
  // здесь большой кусок кода, использующий Num1 и Num2 и ранее заданные переменные
  // поэтому вынести его в отдельную функцию (все равно проинлайнится при компилировании) крайне проблематично
  // - понадобится прописывать очень много входных параметров.
  // ....
    }

На самом деле это решается очень просто: создаются структуры, в которые входят группы объединённых неким образом параметров (лучше логически, а не "по принуждению", ради компактности) и работаем уже с этими структурами: объявляем переменные на их основе, передаём в методы/функции и т.д. Можно делать вложенные структуры, если это требуется.

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

По теме топика: я считаю, что в первую очередь следует думать о дальнейшей поддержке продукта. Поймёте ли Вы свой код через неделю, месяц, год, пять лет? А другой человек, если будет такая потребность?
А если будет нужно внести изменения в копипастный кусок кода, Вы уверены, что ничего не забудете и покроете все требуемые участки? Человеческий фактор никто не отменял... лучше минимизировать его возможное влияние, избегая любых дублей.
И лишь когда 100% будет уверенность в повышенных требованиях к производительности (или когда 100% будет видна "тормознутость" работы), то следует делать какие-то ненормальные (в смысле не такие, как в обычной работе над кодом) телодвижения. И то, нужно грамотно определить, какой участок кода требует оптимизации, а это лучше делать с хорошо написанным кодом :)
Разумеется, ИМХО.

 
ENSED:

Странно, что никто не обратил внимание, но...

На самом деле это решается очень просто: создаются структуры, в которые входят группы объединённых неким образом параметров (лучше логически, а не "по принуждению", ради компактности) и работаем уже с этими структурами: объявляем переменные на их основе, передаём в методы/функции и т.д. Можно делать вложенные структуры, если это требуется.

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

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

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

 
lob32371:

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

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

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

Я говорю только о лаконичности передачи параметров, а не о том, как оно там устроено. С точки зрения управления сложностью есть огромная разница передать, к примеру, 40 параметров, или 7.
Да, инициализировать всё равно будет нужно каждый элемент структуры, но это тоже можно вынести в отдельные методы. А это повышает удобство поддержки кода в будущем :)

Уже сколько лет успешно такой подход применяю в коммерческой разработке, в том числе и командной (лет 7 на C++ преимущественно, последние годы на MQL5 и теперь вот на MQL4), никаких проблем не испытываю ни я, ни коллеги. Даже скорее наоборот. И никто не воспринимает это как самообман, просто популярная практика решения конкретной проблемы "как передать вместо 40 параметров 7, упростив понимание кода" :)

А можно ещё и в классы упаковывать, но это отдельный разговор. 

Но это всё моё ИМХО, хотя и довольно часто встречающаяся практика. 

 

Что до макросов - не уверен, что это универсальное решение на все случаи жизни. Но если это применимо в Ваших конкретных задачах, то я только за. Хотя лично я стараюсь их использовать с осторожностью, но быть может потому, что мало где в работе сталкивался за свои годы практики и просто неопытен в этом направлении.
Я же о более общих случаях говорю, и о конкретной проблеме - число параметров в методах/функциях. И предлагаю вариант решения этой проблемы; довольно популярный вариант, следует заметить.

 
lob32371:

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

Такие ситуации встречаются довольно часто. Вот пример довольно распространенного случая:

// Красивый/лаконичный код
void Calculate1( const bool FlagSource )
{
// здесь определяется много переменных для дальнейших вычислений
// ....

  int Num1, Num2;
  
  for (int i = 0; i < AMOUNT; i++)
  {
    GetNums(FlagSource, i, Num1, Num2);
    
// здесь большой кусок вычислительного кода, использующий Num1 и Num2 и ранее заданные переменные
// поэтому вынести его в отдельную функцию (все равно проинлайнится при компилировании) крайне проблематично
// - понадобится прописывать очень много входных параметров.
// ....
  }
  
  return;
}

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

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

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

Функция GetNums() - довольно проста и реализуется относительно в небольшой кусочек кода. Тем более незначительна будет и разница в эффективности конечного кода, сгенерированного для неё компилятором, в зависимости от реализации этой функции (пусть разница будет оценена, для примера, в 10%). В комментарии для гипотетического кода после вызова данной функции сказано:

// здесь большой кусок вычислительного кода, использующий Num1 и Num2 и ранее заданные переменные
// поэтому вынести его в отдельную функцию (все равно проинлайнится при компилировании) крайне проблематично
// - понадобится прописывать очень много входных параметров.
// ....

Если здесь будет большой кусок вычислительного кода, который по объёму работы для процессора в очень большое число раз (пусть разница здесь будет оценена, скажем, в 100 раз) затратнее функции GetNums(), то какой смысл бороться за оптимизацию, которая составит, для принятых оценок, 1/1000, или 0.1%?

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

Очень важно правильно структурировать данные. Здесь используются, по сути, две одинаковые структуры, но, с точки зрения языка, они - разные. Поэтому не удаётся "подсунуть" нужный массив в самом начале (в MQL4 обязательна функция-обёртка, потому что отсутствует как адресная арифметика, так и ссылки/указатели на массив кроме случая ссылки на массив при передаче параметра функции). Скажем, если бы использовалась одна структура данных для обоих массивов:

struct STRUCT2
{
  int min, max;
};

STRUCT2 Source1[AMOUNT];
STRUCT2 Source2[AMOUNT];

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

// Красивый/лаконичный код
void CalculateMain( const STRUCT2 &Source[] )
{
// здесь определяется много переменных для дальнейших вычислений
// ....

  for (int i = 0; i < AMOUNT; i++)
  {
    int Num1 = Source[i].min;
    int Num2 = Source[i].max;
    
// здесь большой кусок вычислительного кода, использующий Num1 и Num2 и ранее заданные переменные
// поэтому вынести его в отдельную функцию (все равно проинлайнится при компилировании) крайне проблематично
// - понадобится прописывать очень много входных параметров.
// ....
  }
  
  return;
}

// Красивый/лаконичный код
void Calculate( const bool FlagSource )
{
  CalculateMain(FlagSource ? Source1 : Source2);

  return;
}

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

К сожалению, в этом, в прямом смысле слова, недоделанном MQL4++ вечно что-нибудь не работает, и воспользоваться красотой и мощью условного выражения в данном случае в MQL4++ нельзя, но вынести за все циклы переключение источника все равно можно, только некрасиво:

// Красивый/лаконичный код
void Calculate( const bool FlagSource )
{
  // CalculateMain(FlagSource ? Source1 : Source2); // Ошибка в MQL4++: '?' - parameter passed as reference, variable expected

  if (FlagSource) {
    CalculateMain(Source1);
  } else {
    CalculateMain(Source2);
  }

  return;
}

Функцию GetNums() можно выполнить лаконичнее:

void GetNums2( const bool FlagSource, const int Pos, int &Num1, int &Num2 )
{
  Num1 = FlagSource ? Source1[Pos].bid : Source2[Pos].min;
  Num2 = FlagSource ? Source1[Pos].ask : Source2[Pos].max;
}

Прогон тремя компиляторами показал, что оптимизация, заключающаяся в однократном вместо двукратного выполнения проверки логического значения переменной FlagSource, выполнена всеми тремя, и код для исходной версии GetNums() и приведённой выше GetNums2() идентичен (хотя для каждого компилятора он слегка свой).

Код же для Calculate1() и Calculate2(), генерируемый компиляторами gcc и clang, идентичен (с точки зрения быстродействия; формально могут быть переставлены команды, и могут использоваться разные регистры), а, вот, код, генерируемый Intel'овским компилятором для этих функций, отличается: проверка логического значения FlagSource не вынесена за циклы в "лаконичной" версии. Однако, даже эта неэффективность выражается всего двумя инструкциями:

        test      r13b, r13b                                    #49.5 Проверить значение в регистре процессора r13b на 0/не 0
        je        ..B3.4        # Prob 50%                      #49.5 Выполнить переход на тело цикла для значения FlagSource, равного false

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

Для сравнения, приведу тело цикла:

..B3.4:                         # Preds ..B3.2
        mov       edx, DWORD PTR [Source2+r12*8]                #49.5  Прочитать в регистр процессора edx значение поля min структуры (смещение 0) из элемента массива по индексу, хранящемуся в регистре процессора r12
        mov       ecx, DWORD PTR [4+Source2+r12*8]              #49.5  Прочитать в регистр процессора ecx значение поля max структуры (смещение 4) из элемента массива по индексу, хранящемуся в регистре процессора r12
        mov       DWORD PTR [rsp], edx                          #49.28 Запомнить в одной части стековой памяти значения поля min
        mov       DWORD PTR [4+rsp], ecx                        #49.34 Запомнить в другой части стековой памяти значения поля max
        lea       rdi, QWORD PTR [rsp]                          #51.5  Загрузить адрес места в стековой памяти, где хранится значение поля min, в регистр, предназначенный для передачи первого параметра функции
        lea       rsi, QWORD PTR [4+rsp]                        #51.5  Загрузить адрес места в стековой памяти, где хранится значение поля max, в регистр, предназначенный для передачи второго параметра функции
        call      f(int&, int&)                                 #51.5  Вызвать функцию
        inc       r12                                           #47.31 Выполнить инкремент индекса элемента в массиве
        cmp       r12, 1000000                                  #47.23 Сравнить с пределом для последующей проверки, а не пора ли уже это всё прекратить
        jl        ..B3.2        # Prob 99%                      #47.23 Выполнить переход на проверку значения FlagSource (приведено выше), хранимого в регистре процессора r13b, если пока ещё не пора прекратить

Здесь уже встречается неоднократное обращение к памяти, причём, с каждой итерацией цикла память всё время по новому адресу. С учётом этого, неоптимизированная проверка значения FlagSource внутри цикла будет занимать, скорее, даже меньше тех оценочных 10%.

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

void f(int &, int &);

void Calculate1( const bool FlagSource )
{
  int Num1, Num2;

  for (int i = 0; i < AMOUNT; i++)
  {
    GetNums(FlagSource, i, Num1, Num2);

    f(Num1, Num2);
  }
}

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

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

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

 
Простак, ты настолько все запутал... 
 
Не спи,- замерзнешь. 
 
lob32371:

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

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

Совсем не самообман (перегруженные функции, если что, то есть, - 3 разных функции f()):

struct S {
        int i0;
        int i1;
        int i2;
        int i3;
        int i4;
        int i5;
        int i6;
        int i7;
        int i8;
        int i9;
};

void f(int &r0, int &r1, int &r2, int &r3, int &r4, int &r5, int &r6, int &r7, int &r8, int &r9);
void f(S &r);

void f() {
        int i0;
        int i1;
        int i2;
        int i3;
        int i4;
        int i5;
        int i6;
        int i7;
        int i8;
        int i9;

        S s;

        f(i0, i1, i2, i3, i4, i5, i6, i7, i8, i9);
        f(s);
}

Все проверенные компиляторы генерируют похожий код:

#       f(i0, i1, i2, i3, i4, i5, i6, i7, i8, i9);
#       Вот, во что выродился этот вызов

        lea       rax, QWORD PTR [76+rsp]                       #33.2
        lea       rdx, QWORD PTR [72+rsp]                       #33.2
        lea       rsi, QWORD PTR [64+rsp]                       #33.2
        lea       rcx, QWORD PTR [68+rsp]                       #33.2
        push      rax                                           #33.2 Через регистры можно передать только первые 6 параметров, остальные 4 приходится передавать через стек
        push      rdx                                           #33.2 Через регистры можно передать только первые 6 параметров, остальные 4 приходится передавать через стек
        push      rcx                                           #33.2 Через регистры можно передать только первые 6 параметров, остальные 4 приходится передавать через стек
        push      rsi                                           #33.2 Через регистры можно передать только первые 6 параметров, остальные 4 приходится передавать через стек
        lea       rdi, QWORD PTR [72+rsp]                       #33.2
        lea       rsi, QWORD PTR [76+rsp]                       #33.2
        lea       rdx, QWORD PTR [80+rsp]                       #33.2
        lea       rcx, QWORD PTR [84+rsp]                       #33.2
        lea       r8, QWORD PTR [88+rsp]                        #33.2
        lea       r9, QWORD PTR [92+rsp]                        #33.2
        call      f(int&, int&, int&, int&, int&, int&, int&, int&, int&, int&)                      #33.2
        add       rsp, 32                                       #33.2 Здесь указатель стека восстанавливается после тех команд push

#       f(s);
#       А - вот, во что этот

        lea       rdi, QWORD PTR [rsp]                          #34.2
        call      f(S&)                                       #34.2

Концептуально-то, может, и одно и то же, но если смотреть практически - совсем нет.

Организация данных важнее кода.

Если же говорить о коде, то функции не должны быть большими. Есть такое эмпирическое правило: любая функция должна помещаться на экране целиком, то есть, иметь не более 50-80 строк.

 

Хм. Вы всегда Ассемблер привлекаете? 

По всякой всячине ...  

 
simpleton:

Функция GetNums() - довольно проста и реализуется относительно в небольшой кусочек кода. Тем более незначительна будет и разница в эффективности конечного кода, сгенерированного для неё компилятором, в зависимости от реализации этой функции (пусть разница будет оценена, для примера, в 10%). В комментарии для гипотетического кода после вызова данной функции сказано:

Если здесь будет большой кусок вычислительного кода, который по объёму работы для процессора в очень большое число раз (пусть разница здесь будет оценена, скажем, в 100 раз) затратнее функции GetNums(), то какой смысл бороться за оптимизацию, которая составит, для принятых оценок, 1/1000, или 0.1%?

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

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

Очень важно правильно структурировать данные. Здесь используются, по сути, две одинаковые структуры, но, с точки зрения языка, они - разные. Поэтому не удаётся "подсунуть" нужный массив в самом начале (в MQL4 обязательна функция-обёртка, потому что отсутствует как адресная арифметика, так и ссылки/указатели на массив кроме случая ссылки на массив при передаче параметра функции). Скажем, если бы использовалась одна структура данных для обоих массивов:

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

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

К сожалению, в этом, в прямом смысле слова, недоделанном MQL4++ вечно что-нибудь не работает, и воспользоваться красотой и мощью условного выражения в данном случае в MQL4++ нельзя, но вынести за все циклы переключение источника все равно можно, только некрасиво:

Функцию GetNums() можно выполнить лаконичнее:

Ну ты решил разобрать напрямую код, который был показан только для демонстрации проблемы. GetNums может выглядеть гораздо силнее, и StructN могут быть ничем не похожими. В общем случае FlagSource -целая, в GetNums вместо if стоит либо switch, либо сложные условные выражения. Короче, пример показывает концепцию, а не конкретную ситуацию.

Прогон тремя компиляторами показал, что оптимизация, заключающаяся в однократном вместо двукратного выполнения проверки логического значения переменной FlagSource, выполнена всеми тремя, и код для исходной версии GetNums() и приведённой выше GetNums2() идентичен (хотя для каждого компилятора он слегка свой).

Да, это очевидная простейшая оптимизация, на которую стоит сразу расчитывать.

Код же для Calculate1() и Calculate2(), генерируемый компиляторами gcc и clang, идентичен (с точки зрения быстродействия; формально могут быть переставлены команды, и могут использоваться разные регистры), а, вот, код, генерируемый Intel'овским компилятором для этих функций, отличается: проверка логического значения FlagSource не вынесена за циклы в "лаконичной" версии. Однако, даже эта неэффективность выражается всего двумя инструкциями:

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

Для сравнения, приведу тело цикла:

Здесь уже встречается неоднократное обращение к памяти, причём, с каждой итерацией цикла память всё время по новому адресу. С учётом этого, неоптимизированная проверка значения FlagSource внутри цикла будет занимать, скорее, даже меньше тех оценочных 10%.

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

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

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

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

Вот и получается, что рассчитывать даже простейшую с точки зрения автора кода оптимизацию в виде выноса const FlagSource (который может быть целым числом в общем случае) за пределы цикла не стоит.

Ну и мне, как интерпретатору, не видится красота в лаконичном коде:

void GetNums2( const bool FlagSource, const int Pos, int &Num1, int &Num2 )
{
  Num1 = FlagSource ? Source1[Pos].bid : Source2[Pos].min;
  Num2 = FlagSource ? Source1[Pos].ask : Source2[Pos].max;
}

Потому что не красиво выполнять двойную проверку неизменного условия. Лаконично - не всегда красиво. Подобный код избегаю, не рассчитывая, что компилятор сообразит.

 
simpleton:

Совсем не самообман (перегруженные функции, если что, то есть, - 3 разных функции f()):

Все проверенные компиляторы генерируют похожий код:

Концептуально-то, может, и одно и то же, но если смотреть практически - совсем нет.

Организация данных важнее кода.

Если же говорить о коде, то функции не должны быть большими. Есть такое эмпирическое правило: любая функция должна помещаться на экране целиком, то есть, иметь не более 50-80 строк.

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

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