Эффективность преждевременного возврата в функции
Это ситуация, с которой я часто встречаюсь как неопытный программист, и мне интересно, в частности, за амбициозный, быстрый проект, который я пытаюсь оптимизировать. Для основных C-подобных языков (C, objC, С++, Java, С# и т.д.) И их обычных компиляторов эти две функции будут работать так же эффективно? Есть ли разница в скомпилированном коде?
void foo1(bool flag)
{
if (flag)
{
//Do stuff
return;
}
//Do different stuff
}
void foo2(bool flag)
{
if (flag)
{
//Do stuff
}
else
{
//Do different stuff
}
}
В принципе, существует ли когда-либо непосредственный бонус/штраф за эффективность при начале break
ing или return
? Как задействован стек? Существуют ли оптимизированные особые случаи? Существуют ли какие-либо факторы (например, вложение или размер "Делать материал" ), которые могут существенно повлиять на это?
Я всегда сторонник улучшения удобочитаемости в отношении небольших оптимизаций (я вижу, что foo1 много с проверкой параметров), но это происходит так часто, что я хотел бы отложить все беспокойство раз и навсегда.
И я знаю о подводных камнях преждевременной оптимизации... тьфу, это некоторые болезненные воспоминания.
EDIT: я принял ответ, но ответ EJP объясняет довольно кратко, почему использование return
практически ничтожно (в сборке return
создает "ветвь" до конца функции, что чрезвычайно быстро. Разветвление изменяет регистр ПК и может также влиять на кеш и конвейер, что довольно незначительно.) В этом случае, в частности, это буквально не имеет значения, поскольку и теги if/else
, и return
создают одну ветвь конец функции.
Ответы
Ответ 1
Нет никакой разницы:
=====> cat test_return.cpp
extern void something();
extern void something2();
void test(bool b)
{
if(b)
{
something();
}
else
something2();
}
=====> cat test_return2.cpp
extern void something();
extern void something2();
void test(bool b)
{
if(b)
{
something();
return;
}
something2();
}
=====> rm -f test_return.s test_return2.s
=====> g++ -S test_return.cpp
=====> g++ -S test_return2.cpp
=====> diff test_return.s test_return2.s
=====> rm -f test_return.s test_return2.s
=====> clang++ -S test_return.cpp
=====> clang++ -S test_return2.cpp
=====> diff test_return.s test_return2.s
=====>
Значение не имеет разницы в сгенерированном коде вообще без оптимизации в двух компиляторах
Ответ 2
Короткий ответ: никакой разницы. Сделайте себе одолжение и перестаньте беспокоиться об этом. Оптимизирующий компилятор почти всегда умнее вас.
Сосредоточьтесь на удобочитаемости и ремонтопригодности.
Если вы хотите посмотреть, что произойдет, постройте их с оптимизацией и посмотрите на выход ассемблера.
Ответ 3
Интересные ответы: хотя я согласен со всеми из них (до сих пор), есть возможные коннотации к этому вопросу, которые до сих пор полностью игнорируются.
Если приведенный выше пример расширен с распределением ресурсов, а затем проверка ошибок с потенциальным результатом освобождения ресурсов, изображение может измениться.
Считайте наивный подход начинающих:
int func(..some parameters...) {
res_a a = allocate_resource_a();
if (!a) {
return 1;
}
res_b b = allocate_resource_b();
if (!b) {
free_resource_a(a);
return 2;
}
res_c c = allocate_resource_c();
if (!c) {
free_resource_b(b);
free_resource_a(a);
return 3;
}
do_work();
free_resource_c(c);
free_resource_b(b);
free_resource_a(a);
return 0;
}
Вышеизложенное представляло бы крайнюю версию стиля возвращения преждевременно. Обратите внимание, как код становится очень повторяющимся и не ремонтируемым с течением времени, когда его сложность возрастает. В настоящее время люди могут использовать обработку исключений, чтобы поймать их.
int func(..some parameters...) {
res_a a;
res_b b;
res_c c;
try {
a = allocate_resource_a(); # throws ExceptionResA
b = allocate_resource_b(); # throws ExceptionResB
c = allocate_resource_c(); # throws ExceptionResC
do_work();
}
catch (ExceptionBase e) {
# Could use type of e here to distinguish and
# use different catch phrases here
# class ExceptionBase must be base class of ExceptionResA/B/C
if (c) free_resource_c(c);
if (b) free_resource_b(b);
if (a) free_resource_a(a);
throw e
}
return 0;
}
Филипп предложил, взглянув на приведенный ниже пример, использовать выключатель/футляр в блоке catch выше. Можно переключаться (typeof (e)), а затем проваливаться через вызовы free_resourcex()
, но это не тривиально и требует рассмотрения дизайна. И помните, что переключатель/случай без перерывов в точности подобен goto с этикетками с цепочкой с цепочкой ниже...
Как отметил Марк Б, в С++ считается хорошим стилем, следуя принципу Инициализация ресурсов:, RAII короче. Суть концепции заключается в том, чтобы использовать объект-экземпляр для получения ресурсов. Затем ресурсы автоматически освобождаются, как только объекты выходят из области действия, и вызываются их деструкторы. Для взаимозаменяемых ресурсов необходимо соблюдать особую осторожность, чтобы обеспечить правильный порядок освобождения и разработать типы объектов, для которых требуемые данные доступны для всех деструкторов.
Или в дни предварительного исключения:
int func(..some parameters...) {
res_a a = allocate_resource_a();
res_b b = allocate_resource_b();
res_c c = allocate_resource_c();
if (a && b && c) {
do_work();
}
if (c) free_resource_c(c);
if (b) free_resource_b(b);
if (a) free_resource_a(a);
return 0;
}
Но этот упрощённый пример имеет несколько недостатков: его можно использовать только в том случае, если выделенные ресурсы не зависят друг от друга (например, он не может использоваться для выделения памяти, а затем открывает дескриптор файла, а затем считывает данные из дескриптора в память), и он не обеспечивает индивидуальные, отличимые коды ошибок в качестве возвращаемых значений.
Быть быстрым (!) кодом, компактным и легко читаемым и расширяемым Линус Торвальдс применяет другой стиль кода ядра, который имеет дело с ресурсами, даже используя печально известный goto, который имеет смысл...:
int func(..some parameters...) {
res_a a;
res_b b;
res_c c;
a = allocate_resource_a() || goto error_a;
b = allocate_resource_b() || goto error_b;
c = allocate_resource_c() || goto error_c;
do_work();
error_c:
free_resource_c(c);
error_b:
free_resource_b(b);
error_a:
free_resource_a(a);
return 0;
}
Суть обсуждения списков рассылки ядра заключается в том, что большинство языковых функций, которые являются "предпочтительными" над оператором goto, представляют собой неявные gotos, такие как огромные древовидные if/else, обработчики исключений, loop/break/continue заявления и т.д. И в приведенном примере goto считается одобренным, поскольку они прыгают только на небольшое расстояние, имеют четкие метки и освобождают код другого беспорядка для отслеживания условий ошибки. Этот вопрос также обсуждался здесь в stackoverflow.
Однако то, что отсутствует в последнем примере, является хорошим способом вернуть код ошибки. Я думал о добавлении result_code++
после каждого вызова free_resource_x()
и возвращении этого кода, но это компенсирует некоторую прирост скорости вышеупомянутого стиля кодирования. И трудно вернуть 0 в случае успеха. Может быть, я просто невообразимый, -)
Итак, да, я думаю, что существует большая разница в вопросе о преждевременном возврате кода или нет. Но я также думаю, что это очевидно только в более сложном коде, который сложнее или невозможно реструктурировать и оптимизировать для компилятора. Как правило, это происходит, когда вступает в игру распределение ресурсов.
Ответ 4
Несмотря на то, что это не очень ответ, производственный компилятор будет намного лучше оптимизирован, чем вы. Я бы предпочел удобочитаемость и поддерживаемость этих оптимизаций.
Ответ 5
Чтобы быть конкретным, return
будет скомпилирован в ветвь до конца метода, где будет инструкция RET
или что бы это ни было. Если вы оставите это, конец блока перед else
будет скомпилирован в ветвь до конца блока else
. Таким образом, вы можете видеть в этом конкретном случае, это не имеет никакого значения.
Ответ 6
Если вы действительно хотите знать, есть ли разница в скомпилированном коде для вашего конкретного компилятора и системы, вам нужно будет скомпилировать и посмотреть на сборку самостоятельно.
Однако в большой схеме вещей почти наверняка, что компилятор может оптимизироваться лучше, чем ваша тонкая настройка, и даже если это не может показаться маловероятным для вашей производительности программы.
Вместо этого напишите код самым ясным способом для людей, чтобы читать и поддерживать, и пусть компилятор делает то, что он делает лучше всего: создайте лучшую сборку, которая может быть из вашего источника.
Ответ 7
В вашем примере заметка заметна. Что происходит с отладкой человека, когда возвращение является страницей или двумя выше/ниже, где//происходит разный материал? Намного сложнее найти/увидеть, когда есть больше кода.
void foo1(bool flag)
{
if (flag)
{
//Do stuff
return;
}
//Do different stuff
}
void foo2(bool flag)
{
if (flag)
{
//Do stuff
}
else
{
//Do different stuff
}
}
Ответ 8
Я полностью согласен с blueshift: читабельность и удобство обслуживания! Но если вы действительно волнуетесь (или просто хотите узнать, что делает ваш компилятор, что, безусловно, хорошая идея в долгосрочной перспективе), вы должны искать себя.
Это будет означать использование декомпилятора или просмотр выходного файла компилятора низкого уровня (например, сборка lanuage). В С# или любом языке .Net инструменты описанные здесь, предоставят вам то, что вам нужно.
Но, как вы сами наблюдали, это, вероятно, преждевременная оптимизация.
Ответ 9
Из Чистый код: руководство по гибкому программному мастерству
Аргументы флагов уродливы. Передача логического элемента в функцию - действительно ужасная практика. Это немедленно усложняет подпись метода, громко заявляя, что эта функция делает больше чем одно. Он делает одно, если флаг является истинным, а другой, если флаг является ложным!
foo(true);
в коде просто заставит читателя перейти к функции и потерять время чтения foo (логический флаг)
Лучшая структурированная база кода даст вам лучшую возможность оптимизировать код.
Ответ 10
Одна школа мысли (не может вспомнить о яйцеголовке, которая предложила ее в данный момент) состоит в том, что вся функция должна иметь только одну точку возврата с структурной точки зрения, чтобы сделать код более легким для чтения и отладки. Это, я полагаю, больше для программирования религиозных дебатов.
Одна техническая причина, по которой вы можете контролировать, когда и как функция выходит, которая нарушает это правило, - это когда вы кодируете приложения реального времени, и вы хотите, чтобы все пути управления через функцию принимали одинаковое количество тактовых циклов для завершения.
Ответ 11
Я рад, что вы подняли этот вопрос. Вы всегда должны использовать ветки над ранним возвратом. Зачем останавливаться? Объедините все свои функции в один, если сможете (по крайней мере, сколько сможете). Это выполнимо, если нет рекурсии. В конце концов, у вас будет одна массивная основная функция, но это то, что вам нужно/нужно для такого рода вещей. После этого переименуйте свои идентификаторы как можно короче. Таким образом, когда ваш код выполняется, меньше времени тратится на чтение имен. Далее выполните...