Эффективность преждевременного возврата в функцию - PullRequest
96 голосов
/ 25 октября 2011

С такой ситуацией я часто сталкиваюсь как неопытный программист, и меня особенно интересует амбициозный, интенсивный по скорости проект, который я пытаюсь оптимизировать. Будут ли эти две функции работать так же эффективно для основных C-подобных языков (C, objC, C ++, Java, C # и т. Д.) И их обычных компиляторов? Есть ли разница в скомпилированном коде?

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 или return рано? Как задействован стек? Есть ли оптимизированные особые случаи? Существуют ли какие-либо факторы (такие как встраивание или размер «Делать вещи»), которые могут существенно повлиять на это?

Я всегда сторонник улучшения разборчивости по сравнению с незначительными оптимизациями (я часто вижу foo1 с проверкой параметров), но это происходит так часто, что я бы хотел раз и навсегда отложить все заботы.

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

РЕДАКТИРОВАТЬ: Я принял ответ, но ответ EJP довольно кратко объясняет, почему использование return практически ничтожно (в сборке return создает «ветвь» в конце функции, что чрезвычайно Быстрая. Ветвь изменяет регистр ПК и может также влиять на кэш и конвейер, что довольно незначительно.) В данном случае, в частности, это буквально не имеет значения, потому что и if/else, и return создают одну и ту же ветку для конец функции.

Ответы [ 11 ]

92 голосов
/ 25 октября 2011

Нет никакой разницы:

=====> 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
=====> 

Это означает отсутствие различий в сгенерированном коде, даже без оптимизации в двух компиляторах

65 голосов
/ 25 октября 2011

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

Сконцентрируйтесь на удобочитаемости и удобстве сопровождения.

Если вы хотите увидеть, что произойдет, создайте их с оптимизацией и посмотрите на вывод ассемблера.

28 голосов
/ 25 октября 2011

Интересные ответы: хотя я и согласен со всеми из них (до сих пор), есть возможные коннотации к этому вопросу, которые до сих пор полностью игнорировались.

Если простой приведенный выше пример расширен за счет выделения ресурсови затем при проверке ошибок с возможным последующим высвобождением ресурсов картина может измениться.

Рассмотрим наивный подход Для начинающих может потребоваться:

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;
}

Филипп, посмотрев на приведенный ниже пример goto, предложил использовать break-меньше выключатель / чехол внутри блока захвата выше.Можно переключиться (typeof (e)) и затем выполнить вызовы free_resourcex(), но это не тривиально и требует рассмотрения проекта .И помните, что переключатель / регистр без разрывов точно такой же, как goto с метками с гирляндными цепями ниже ...

Как отметил Марк B, в C ++ считается хорошим стилем следовать Resource AquisitionИнициализация принцип, 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Это неявные goto, такие как огромные, древовидные if / else, обработчики исключений, операторы loop / break / continue и т. д. И goto в приведенном выше примере считаются нормальными, так как они прыгают только на небольшое расстояние, имеют четкие меткии освободите код другого беспорядка для отслеживания условий ошибки. Этот вопрос также обсуждался здесь на stackoverflow .

Однако то, что отсутствует в последнем примере, - это хороший способ вернуть код ошибки.Я думал о добавлении result_code++ после каждого free_resource_x() вызова и возврате этого кода, но это компенсирует некоторые выигрыши в скорости вышеописанного стиля кодирования.И трудно вернуть 0 в случае успеха.Может быть, я просто не обладаю воображением; -)

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

12 голосов
/ 25 октября 2011

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

9 голосов
/ 25 октября 2011

Если говорить конкретно об этом, return будет скомпилирован в ветвь до конца метода, где будет инструкция RET или что бы то ни было.Если вы пропустите это, конец блока перед else будет скомпилирован в ветвь до конца else блока.Таким образом, вы можете видеть, что в этом конкретном случае нет никакой разницы.

4 голосов
/ 25 октября 2011

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

void foo1(bool flag)
{
    if (flag)
    {
        //Do stuff
        return;
    }

    //Do different stuff
}

void foo2(bool flag)
{
    if (flag)
    {
        //Do stuff
    }
    else
    {
        //Do different stuff
    }
}
4 голосов
/ 25 октября 2011

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

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

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

3 голосов
/ 25 октября 2011

Я полностью согласен с blueshift: удобочитаемость и ремонтопригодность в первую очередь !.Но если вы действительно волнуетесь (или просто хотите узнать, что делает ваш компилятор, что, безусловно, хорошая идея в долгосрочной перспективе), вы должны искать сами.

Это будет означать использование декомпилятора или просмотр низкоуровневого вывода компилятора (например, языка сборки).В C # или любом другом языке .Net инструменты, описанные здесь , дадут вам то, что вам нужно.

Но, как вы сами заметили, это, вероятно, преждевременная оптимизация.

1 голос
/ 26 октября 2011

С Чистый код: руководство по мастерству гибкого программного обеспечения

Аргументы флага ужасны. Передача логического выражения в функцию - это действительно ужасная практика. Это сразу усложняет подпись метода, громко заявляя, что эта функция делает больше, чем одну вещь. Он делает одно, если флаг имеет значение true, и другое, если флаг имеет значение false!

foo(true);

в коде просто заставит читателя переходить к функции и тратит время на чтение foo (логический флаг)

Лучшая структура структурированного кода даст вам больше возможностей для оптимизации кода.

0 голосов
/ 26 октября 2011

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

...