Эффективность преждевременного возврата в функции

Это ситуация, с которой я часто встречаюсь как неопытный программист, и мне интересно, в частности, за амбициозный, быстрый проект, который я пытаюсь оптимизировать. Для основных 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

Я рад, что вы подняли этот вопрос. Вы всегда должны использовать ветки над ранним возвратом. Зачем останавливаться? Объедините все свои функции в один, если сможете (по крайней мере, сколько сможете). Это выполнимо, если нет рекурсии. В конце концов, у вас будет одна массивная основная функция, но это то, что вам нужно/нужно для такого рода вещей. После этого переименуйте свои идентификаторы как можно короче. Таким образом, когда ваш код выполняется, меньше времени тратится на чтение имен. Далее выполните...