Распространенная ситуация при программинге, как быть? Быстродействие vs Лаконичность - страница 2
Вы упускаете торговые возможности:
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Регистрация
Вход
Вы принимаете политику сайта и условия использования
Если у вас нет учетной записи, зарегистрируйтесь
Странно, что никто не обратил внимание, но...
На самом деле это решается очень просто: создаются структуры, в которые входят группы объединённых неким образом параметров (лучше логически, а не "по принуждению", ради компактности) и работаем уже с этими структурами: объявляем переменные на их основе, передаём в методы/функции и т.д. Можно делать вложенные структуры, если это требуется.
Почти всегда это может дать нужный результат и открывает путь к лаконичной декомпозиции.
По теме топика: я считаю, что в первую очередь следует думать о дальнейшей поддержке продукта. Поймёте ли Вы свой код через неделю, месяц, год, пять лет? А другой человек, если будет такая потребность?
А если будет нужно внести изменения в копипастный кусок кода, Вы уверены, что ничего не забудете и покроете все требуемые участки? Человеческий фактор никто не отменял... лучше минимизировать его возможное влияние, избегая любых дублей.
И лишь когда 100% будет уверенность в повышенных требованиях к производительности (или когда 100% будет видна "тормознутость" работы), то следует делать какие-то ненормальные (в смысле не такие, как в обычной работе над кодом) телодвижения. И то, нужно грамотно определить, какой участок кода требует оптимизации, а это лучше делать с хорошо написанным кодом :)
Разумеется, ИМХО.
Странно, что никто не обратил внимание, но...
На самом деле это решается очень просто: создаются структуры, в которые входят группы объединённых неким образом параметров (лучше логически, а не "по принуждению", ради компактности) и работаем уже с этими структурами: объявляем переменные на их основе, передаём в методы/функции и т.д. Можно делать вложенные структуры, если это требуется.
Почти всегда это может дать нужный результат и открывает путь к лаконичной декомпозиции.
Ваше предложение - самообман. Что я определю входные параметры в одной структуре и передам ее в функцию, что напрямую - суть, одно и то же.
На самом деле решение было предложено до замечательного ответа Pavlick - макросы. Тогда ни о каких входных параметрах даже речь вести не нужно.
Ваше предложение - самообман. Что я определю входные параметры в одной структуре и передам ее в функцию, что напрямую - суть, одно и то же.
На самом деле решение было предложено до замечательного ответа Pavlick - макросы. Тогда ни о каких входных параметрах даже речь вести не нужно.
Этот "самообман" называется декомпозицией, важный элемент управления сложностью. Машине плевать, а человеку такое воспринимать и поддерживать намного легче :)
Я говорю только о лаконичности передачи параметров, а не о том, как оно там устроено. С точки зрения управления сложностью есть огромная разница передать, к примеру, 40 параметров, или 7.
Да, инициализировать всё равно будет нужно каждый элемент структуры, но это тоже можно вынести в отдельные методы. А это повышает удобство поддержки кода в будущем :)
Уже сколько лет успешно такой подход применяю в коммерческой разработке, в том числе и командной (лет 7 на C++ преимущественно, последние годы на MQL5 и теперь вот на MQL4), никаких проблем не испытываю ни я, ни коллеги. Даже скорее наоборот. И никто не воспринимает это как самообман, просто популярная практика решения конкретной проблемы "как передать вместо 40 параметров 7, упростив понимание кода" :)
А можно ещё и в классы упаковывать, но это отдельный разговор.
Но это всё моё ИМХО, хотя и довольно часто встречающаяся практика.
Что до макросов - не уверен, что это универсальное решение на все случаи жизни. Но если это применимо в Ваших конкретных задачах, то я только за. Хотя лично я стараюсь их использовать с осторожностью, но быть может потому, что мало где в работе сталкивался за свои годы практики и просто неопытен в этом направлении.
Я же о более общих случаях говорю, и о конкретной проблеме - число параметров в методах/функциях. И предлагаю вариант решения этой проблемы; довольно популярный вариант, следует заметить.
Вечная дилема между красивым/лаконичным кодом и быстродействующим у каждого решается по-своему. Очень часто многое отдается на откуп компилятору с надеждой, что лаконичный код будет компилятором прооптимизирован (через инлайнинг и прочее) до состояния быстродействующего варианта.
Такие ситуации встречаются довольно часто. Вот пример довольно распространенного случая:
Пояснения и вопросы в комментариях. Кракто, как в такой ситуации добиться быстродействия и не потерять в стройности кода? Пришло на ум использование макросов.
В общем, поделититесь своим опытом и рекомендациями, как действовать, когда, например, всего лишь из-за одного if (как в примере) приходится для быстродействия копи-пастить огромную часть кода, создавая монстрский код.
Очевидно, подобных ситуаций можно придумать множество. Какие общие могут быть рекомендации? Дайте ссылки на обсуждения этой поросшей мхом проблемы.
Функция GetNums() - довольно проста и реализуется относительно в небольшой кусочек кода. Тем более незначительна будет и разница в эффективности конечного кода, сгенерированного для неё компилятором, в зависимости от реализации этой функции (пусть разница будет оценена, для примера, в 10%). В комментарии для гипотетического кода после вызова данной функции сказано:
Если здесь будет большой кусок вычислительного кода, который по объёму работы для процессора в очень большое число раз (пусть разница здесь будет оценена, скажем, в 100 раз) затратнее функции GetNums(), то какой смысл бороться за оптимизацию, которая составит, для принятых оценок, 1/1000, или 0.1%?
Такая оптимизация, например, если неоптимизированный код на рабочих данных исполняется в течение часа, даст выигрыш для оптимизированного - менее 4-х секунд. Стоит ли оно того?
Очень важно правильно структурировать данные. Здесь используются, по сути, две одинаковые структуры, но, с точки зрения языка, они - разные. Поэтому не удаётся "подсунуть" нужный массив в самом начале (в MQL4 обязательна функция-обёртка, потому что отсутствует как адресная арифметика, так и ссылки/указатели на массив кроме случая ссылки на массив при передаче параметра функции). Скажем, если бы использовалась одна структура данных для обоих массивов:
то можно было бы, используя красивую лаконичную мощь условного выражения, "разветвиться по данным" на раннем этапе, то есть, легко самому вынести эту операцию за все циклы и не беспокоиться, справится ли компилятор:
Даже в данном случае проявился факт того, что правильная организация данных важнее правильно разработанного кода, который работает с этими данными.
К сожалению, в этом, в прямом смысле слова, недоделанном MQL4++ вечно что-нибудь не работает, и воспользоваться красотой и мощью условного выражения в данном случае в MQL4++ нельзя, но вынести за все циклы переключение источника все равно можно, только некрасиво:
Функцию GetNums() можно выполнить лаконичнее:
Прогон тремя компиляторами показал, что оптимизация, заключающаяся в однократном вместо двукратного выполнения проверки логического значения переменной FlagSource, выполнена всеми тремя, и код для исходной версии GetNums() и приведённой выше GetNums2() идентичен (хотя для каждого компилятора он слегка свой).
Код же для Calculate1() и Calculate2(), генерируемый компиляторами gcc и clang, идентичен (с точки зрения быстродействия; формально могут быть переставлены команды, и могут использоваться разные регистры), а, вот, код, генерируемый Intel'овским компилятором для этих функций, отличается: проверка логического значения FlagSource не вынесена за циклы в "лаконичной" версии. Однако, даже эта неэффективность выражается всего двумя инструкциями:
Первая инструкция проверяет значение в регистре, а вторая осуществляет условный переход. Ни одна из инструкций не обращается к памяти, что существенно повышает быстродействие этой части кода.
Для сравнения, приведу тело цикла:
Здесь уже встречается неоднократное обращение к памяти, причём, с каждой итерацией цикла память всё время по новому адресу. С учётом этого, неоптимизированная проверка значения FlagSource внутри цикла будет занимать, скорее, даже меньше тех оценочных 10%.
С целью не дать компилятору выполнить лишние оптимизации из-за отсутствия "большого куска вычислительного кода", этот кусок был проэмулирован вызовом функции, реализация которой недоступна в данной единице трансляции. Недоступность кода функции не оставляет никаких шансов компилятору выполнить лишнюю оптимизацию. Видоизменённый код, использованный для получения приведённого выше ассемблерного листинга:
В итоге получается, что как минимум в простых случаях компиляторы, но не все, могут самостоятельно выполнить оптимизации типа выноса вычисления неизменяемого в цикле значения за цикл.
Но лучше, если удаётся построить код оптимальным образом самостоятельно, а не надеяться на компиляторы. А для этого, как правило, требуется правильно организовать и структурировать данные, с которыми работает код.
Также важно оценить, насколько оптимизация в конкретном место влияет на общую производительность, и стоит ли поэтому здесь игра свеч.
Ваше предложение - самообман. Что я определю входные параметры в одной структуре и передам ее в функцию, что напрямую - суть, одно и то же.
На самом деле решение было предложено до замечательного ответа Pavlick - макросы. Тогда ни о каких входных параметрах даже речь вести не нужно.
Совсем не самообман (перегруженные функции, если что, то есть, - 3 разных функции f()):
Все проверенные компиляторы генерируют похожий код:
Концептуально-то, может, и одно и то же, но если смотреть практически - совсем нет.
Организация данных важнее кода.
Если же говорить о коде, то функции не должны быть большими. Есть такое эмпирическое правило: любая функция должна помещаться на экране целиком, то есть, иметь не более 50-80 строк.
Хм. Вы всегда Ассемблер привлекаете?
По всякой всячине ...
Функция 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 (который может быть целым числом в общем случае) за пределы цикла не стоит.
Ну и мне, как интерпретатору, не видится красота в лаконичном коде:
Потому что не красиво выполнять двойную проверку неизменного условия. Лаконично - не всегда красиво. Подобный код избегаю, не рассчитывая, что компилятор сообразит.
Совсем не самообман (перегруженные функции, если что, то есть, - 3 разных функции f()):
Все проверенные компиляторы генерируют похожий код:
Концептуально-то, может, и одно и то же, но если смотреть практически - совсем нет.
Организация данных важнее кода.
Если же говорить о коде, то функции не должны быть большими. Есть такое эмпирическое правило: любая функция должна помещаться на экране целиком, то есть, иметь не более 50-80 строк.
Концептуально - одно и то же. Реализации, конечно, могут быть разными. Но опять же, мы говорим про концепции на уровне какого-то шестого чувства...
У меня редко бывают функции, противоречащие этому правилу. Но есть образцы, которые можно назвать лаконичными, очень красивыми и при этом за сотню строчек. И там любая попытка превратить часть кода в вызов функций выглядит неуместной.