Выход из вложенного цикла


Если у меня есть цикл for, который вложен в другой, как я могу эффективно выйти из обоих циклов (внутренний и внешний) как можно быстрее?

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

Что это быстрый и хороший способ идти об этом?

спасибо


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

Я не чувствую, что это правильно, чтобы воспользоваться новыми функциями в .NET (методы anon), чтобы сделать что-то, что является довольно фундаментальным.

из-за этого, tvon (извините не могу написать полное имя пользователя!) имеет хорошее решение.

Марк: хорошее использование методов anon, и это тоже здорово, но потому что я мог бы быть на работе, где мы этого не делаем используйте версию .NET / C#, которая поддерживает методы anon, мне тоже нужно знать традиционный подход.

9 185

9 ответов:

Ну goto, но это некрасиво, и не всегда возможно. Вы также можете поместить циклы в метод (или anon-метод) и использовать return чтобы вернуться к основному коду.

    // goto
    for (int i = 0; i < 100; i++)
    {
        for (int j = 0; j < 100; j++)
        {
            goto Foo; // yeuck!
        }
    }
Foo:
    Console.WriteLine("Hi");

vs:

// anon-method
Action work = delegate
{
    for (int x = 0; x < 100; x++)
    {
        for (int y = 0; y < 100; y++)
        {
            return; // exits anon-method
        }
    }
};
work(); // execute anon-method
Console.WriteLine("Hi");

обратите внимание, что в C# 7 мы должны получить "локальные функции", что (синтаксис tbd и т. д.) означает, что он должен работать примерно так:

// local function (declared **inside** another method)
void Work()
{
    for (int x = 0; x < 100; x++)
    {
        for (int y = 0; y < 100; y++)
        {
            return; // exits local function
        }
    }
};
Work(); // execute local function
Console.WriteLine("Hi");

Не знаю, если это работает в C#, но в C я часто делаю так:

    for (int i = 0; i < 100; i++)
    {
        for (int j = 0; j < 100; j++)
        {
            if (exit_condition)
            {
                // cause the outer loop to break:
                i = INT_MAX;
                Console.WriteLine("Hi");
                // break the inner loop
                break;
            }
        }
    }

это решение не относится к C#

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

outer: while(fn1())
{
   while(fn2())
   {
     if(fn3()) continue outer;
     if(fn4()) break outer;
   }
}

используйте соответствующий предохранитель в наружной петле. Установите предохранитель во внутреннюю петлю, прежде чем сломать.

bool exitedInner = false;

for (int i = 0; i < N && !exitedInner; ++i) {

    .... some outer loop stuff

    for (int j = 0; j < M; ++j) {

        if (sometest) {
            exitedInner = true;
            break;
        }
    }
    if (!exitedInner) {
       ... more outer loop stuff
    }
}

или еще лучше, абстрагировать внутренний цикл в метод и выйти из внешнего цикла, когда он возвращает false.

for (int i = 0; i < N; ++i) {

    .... some outer loop stuff

    if (!doInner(i, N, M)) {
       break;
    }

    ... more outer loop stuff
}

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

Гото:

for ( int i = 0; i < 10; ++i ) {
   for ( int j = 0; j < 10; ++j ) {
      // code
      if ( break_condition ) goto End;
      // more code
   }
}
End: ;

состояние:

bool exit = false;
for ( int i = 0; i < 10 && !exit; ++i ) {
   for ( int j = 0; j < 10 && !exit; ++j ) {
      // code
      if ( break_condition ) {
         exit = true;
         break; // or continue
      }
      // more code
   }
}

исключения:

try {
    for ( int i = 0; i < 10 && !exit; ++i ) {
       for ( int j = 0; j < 10 && !exit; ++j ) {
          // code
          if ( break_condition ) {
             throw new Exception()
          }
          // more code
       }
    }
catch ( Exception e ) {}

можно ли изменить вложенный цикл for в отдельный метод? Таким образом, вы можете просто "вернуться" из метода, чтобы выйти из цикла.

фактор в функцию / метод и использовать раннее возвращение, или перестроить свои циклы в предложение while. goto / исключения / все, что здесь, конечно, не подходит.

def do_until_equal():
  foreach a:
    foreach b:
      if a==b: return

вы попросили комбинацию быстрого, приятного, без использования логического значения, без использования goto и C#. Вы исключили все возможные способы делать то, что вы хотите.

самый быстрый и наименее уродливый способ-использовать goto.

мне кажется, что люди не любят a goto заявление много, поэтому я чувствовал необходимость немного исправить это.

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

как мы все знаем, C# компилируется в IL, который затем компилируется в ассемблер с использованием компилятора SSA. Я дам немного информации о том, как все это работает, а затем попытаюсь ответить на сам вопрос.

от C# до IL

Сначала нам нужен кусок кода на C#. Давайте начнем просто:

foreach (var item in array)
{
    // ... 
    break;
    // ...
}

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

первый перевод: с foreach в эквиваленте for цикл (Примечание: я использую массив здесь, потому что я не делаю хотите узнать подробности IDisposable - в этом случае мне также придется использовать IEnumerable):

for (int i=0; i<array.Length; ++i)
{
    var item = array[i];
    // ...
    break;
    // ...
}

второй перевод: the for и break переводится в более простой эквивалент:

int i=0;
while (i < array.Length)
{
    var item = array[i];
    // ...
    break;
    // ...
    ++i;
}

и третий перевод (это эквивалент кода IL): мы меняем break и while в филиал:

    int i=0; // for initialization

startLoop:
    if (i >= array.Length) // for condition
    {
        goto exitLoop;
    }
    var item = array[i];
    // ...
    goto exitLoop; // break
    // ...
    ++i;           // for post-expression
    goto startLoop; 

в то время как компилятор делает эти вещи в один шаг, это дает вам представление о процессе. Код IL, который эволюционирует из программы C# - это дословный перевод последнего кода C#. Вы можете увидеть сами здесь:https://dotnetfiddle.net/QaiLRz (нажмите "посмотреть IL")

теперь, одна вещь, которую вы заметили здесь, заключается в том, что во время процесса код становится более сложным. Самый простой способ наблюдать это-тот факт, что нам нужно было все больше и больше кода, чтобы завершить то же самое. Вы также можете утверждать, что foreach,for,while и break несколько на самом деле короткие руки для goto, что отчасти верно.

от IL к ассемблеру

интернет .Чистый JIT-компилятор-это компилятор ССА. Я не буду вдаваться во все детали формы SSA здесь и как создать оптимизирующий компилятор, это просто слишком много, но может дать общее представление о том, что произойдет. Для более глубокого понимания лучше всего начать читать об оптимизации компиляторов (мне нравится эта книга для краткого введения: http://ssabook.gforge.inria.fr/latest/book.pdf) и LLVM (llvm.org).

каждый оптимизирующий компилятор полагается на то, что код легко и ниже предсказуемые схемы. В случае циклов FOR мы используем теорию графов для анализа ветвей, а затем оптимизируем такие вещи, как циклы в наших ветвях (например, ветви назад).

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

    int i=0; // for initialization

    if (i >= array.Length) // for condition
    {
        goto endOfLoop;
    }

startLoop:
    var item = array[i];
    // ...
    goto endOfLoop; // break
    // ...
    ++i;           // for post-expression

    if (i >= array.Length) // for condition
    {
        goto startLoop;
    }

endOfLoop:
    // ...

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

так почему компилятор делает это? Ну, если мы сможем развернуть цикл, мы сможем его векторизовать. Мы могли бы даже быть в состоянии доказать, что есть только константы добавляются, что означает, что весь наш цикл может исчезнуть в воздухе. Подводя итог: делая шаблоны предсказуемыми (делая ветви предсказуемыми), мы можем доказать, что определенные условия сохраняются в нашем цикле, что означает, что мы можем делать магию во время JIT-оптимизации.

тем не менее, ветви, как правило, нарушают эти приятные предсказуемые Шаблоны, что является чем-то оптимизатором, поэтому вид-Нелюбовь. Перерыв, продолжить, Гото-они все намерены чтобы сломать эти предсказуемые шаблоны-и поэтому не очень "приятно".

вы также должны понимать, что простой foreach более предсказуемо, чем куча goto заявления, которые идут повсюду. С точки зрения (1) читаемости и (2) с точки зрения оптимизатора, это лучшее решение.

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

помогите, слишком много сложностей... что же мне делать?

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

один из этих шаблонов называется SESE, который стоит для одиночного входа одиночный выход.

а теперь перейдем к реальному вопросу.

представьте, что у вас что-то вроде этого:

// a is a variable.

for (int i=0; i<100; ++i) 
{
  for (int j=0; j<100; ++j)
  {
     // ...

     if (i*j > a) 
     {
        // break everything
     }
  }
}

самый простой способ сделать это предсказуемым шаблоном-просто устранить if полностью:

int i, j;
for (i=0; i<100 && i*j <= a; ++i) 
{
  for (j=0; j<100 && i*j <= a; ++j)
  {
     // ...
  }
}

в других случаях вы также можете разделить метод на 2 метода:

// Outer loop in method 1:

for (i=0; i<100 && processInner(i); ++i) 
{
}

private bool processInner(int i)
{
  int j;
  for (j=0; j<100 && i*j <= a; ++j)
  {
     // ...
  }
  return i*j<=a;
}

временные переменные? Хорошо, плохо или некрасиво?

вы может даже решить вернуть логическое значение из цикла (но я лично предпочитаю форму SESE, потому что именно так компилятор увидит его, и я думаю, что это чище читать).

некоторые люди думают, что это чище, чтобы использовать временную переменную, и предложить вариант такой:

bool more = true;
for (int i=0; i<100; ++i) 
{
  for (int j=0; j<100; ++j) 
  {
     // ...
     if (i*j > a) { more = false; break; } // yuck.
     // ...
  }
  if (!more) { break; } // yuck.
  // ...
}
// ...

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

хорошо, позвольте мне объяснить это. Что произойдет, так это:

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