- PVSM.RU - https://www.pvsm.ru -

Эта статья — краткая заметка о двух связанных друг с другом эмпирических правилах.
Если внутри функции есть условие if, то подумайте, нельзя ли его переместить в вызывающую сторону:
// ХОРОШО
fn frobnicate(walrus: Walrus) {
...
}
// ПЛОХО
fn frobnicate(walrus: Option<Walrus>) {
let walrus = match walrus {
Some(it) => it,
None => return,
};
...
}
В подобных примерах часто существуют предварительные условия: функция может проверять предусловие внутри и «ничего не делать», если оно не выполняется, или же может передать задачу проверки предварительного условия вызывающей её стороне, а при помощи типов (или assert) принудительно удовлетворить этому условию. Подъём проверок вверх, особенно в случае предварительных условий, может иметь лавинообразный эффект и привести к уменьшению общего количества проверок. Именно поэтому и возникло это правило.
Ещё одна причина заключается в том, что поток управления и if сложны, к тому же оказываются источниками багов. Поднимая if наверх, мы часто приходим к централизации потока управления в одной функции, имеющей сложную логику ветвления, но сама работа при этом делегируется к простым линейным подпрограммам.
Если у вас есть сложный поток управления, то лучше перенести его в одну функцию, умещающуюся на одном экране, а не разбрасывать его по файлу. Более того, если весь поток находится в одном месте, то часто можно заменить избыточность и невыполняющиеся условия. Сравните:
fn f() {
if foo && bar {
if foo {
} else {
}
}
}
fn g() {
if foo && bar {
h()
}
}
fn h() {
if foo {
} else {
}
}
В случае f гораздо проще заметить «мёртвое» ветвление, чем в последовательности g и h!
Есть и другой схожий паттерн, который я называю рефакторингом «растворяющихся enum». Иногда код начинает выглядеть так:
enum E {
Foo(i32),
Bar(String),
}
fn main() {
let e = f();
g(e)
}
fn f() -> E {
if condition {
E::Foo(x)
} else {
E::Bar(y)
}
}
fn g(e: E) {
match e {
E::Foo(x) => foo(x),
E::Bar(y) => bar(y)
}
}
Здесь две команды ветвления; если поднять их наверх, то становится очевидно, что это одно и то же условие, которое повторяется трижды (в третий раз оно превращается в структуру данных):
fn main() {
if condition {
foo(x)
} else {
bar(y)
}
}
Этот совет взят из подхода, ориентированного на обработку данных. Программы обычно работают с сериями объектов, или, по крайней мере, горячий путь выполнения обычно связан с обработкой множества сущностей. Именно из-за количества сущностей путь и становится в первую очередь горячим. Поэтому часто бывает разумно ввести концепцию «группы» объектов и в базовом случае выполнять операции с группами, а скалярная версия при этом будет особым случаем групповой обработки:
// ХОРОШО
frobnicate_batch(walruses)
// ПЛОХО
for walrus in walruses {
frobnicate(walrus)
}
Основное преимущество здесь — производительность. А крайних случаях [1] — огромный рост производительности.
Если у вас есть целая группа объектов для обработки, можно амортизировать затраты на запуск и более гибко определять порядок обработки. На самом деле, вам даже не нужно обрабатывать сущности в каком-либо конкретном порядке: можно реализовать трюк с векторизацией/массивом структур, чтобы сначала обрабатывать одно поле всех сущностей, а затем переходить к другим полям.
Наверно, самый забавный пример здесь — это умножение многочленов на основании быстрого преобразования Фурье [2]: оказывается, одновременное вычисление многочлена в куче точек можно выполнять быстрее, чем кучу вычислений отдельных точек!
Два эти совета про for и if даже можно комбинировать!
// ХОРОШО
if condition {
for walrus in walruses {
walrus.frobnicate()
}
} else {
for walrus in walruses {
walrus.transmogrify()
}
}
// ПЛОХО
for walrus in walruses {
if condition {
walrus.frobnicate()
} else {
walrus.transmogrify()
}
}
Версия ХОРОШО хороша тем, что ей не приходится многократно проверять condition, она избавляется от ветвления в горячем цикле, а потенциально и обеспечивает возможность векторизации. Этот паттерн работает и на микро-, и на макроуровне — хорошей версией архитектуры можно считать TigerBeetle, в которой в плоскости данных мы одновременно работаем с группами объектов, чтобы амортизировать стоимость принятия решений в плоскости управления.
Хотя совет про for в первую очередь связан с повышением производительности, иногда он и улучшает выразительность. Когда-то был довольно успешен jQuery, работавший с коллекциями элементов. Язык абстрактных векторных пространств чаще оказывается более удобным инструментом, чем куча уравнений с координатами.
Подведём итог: поднимайте if наверх и опускайте for вниз!
Автор: PatientZero
Источник [3]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/optimizatsiya-koda/420607
Ссылки в тексте:
[1] крайних случаях: http://venge.net/graydon/talks/VectorizedInterpretersTalk-2023-05-12.pdf
[2] умножение многочленов на основании быстрого преобразования Фурье: https://en.wikipedia.org/wiki/Sch%C3%B6nhage%E2%80%93Strassen_algorithm
[3] Источник: https://habr.com/ru/articles/911790/?utm_source=habrahabr&utm_medium=rss&utm_campaign=911790
Нажмите здесь для печати.