Бросок против Ретроу: тот же результат?

ссылаясь на большое количество документации в сети, в частности, на SO, например: Как правильно вызвать исключение в С#? должна быть разница между "throw e"; и "бросить;".

Но из: http://bartdesmet.net/blogs/bart/archive/2006/03/12/3815.aspx,

этот код:

using System;

class Ex
{
   public static void Main()
  {
  //
  // First test rethrowing the caught exception variable.
  //
  Console.WriteLine("First test");
  try
  {
     ThrowWithVariable();
  }
  catch (Exception ex)
  {
     Console.WriteLine(ex.StackTrace);
  }

  //
  // Second test performing a blind rethrow.
  //
  Console.WriteLine("Second test");
  try
  {
     ThrowWithoutVariable();
  }
  catch (Exception ex)
  {
     Console.WriteLine(ex.StackTrace);
  }
}

 private static void BadGuy()
 {
   //
   // Some nasty behavior.
  //
   throw new Exception();
 }

   private static void ThrowWithVariable()
 {
   try
   {
         BadGuy();
   }
  catch (Exception ex)
  {
     throw ex;
  }
}

   private static void ThrowWithoutVariable()
{
  try
  {
     BadGuy();
  }
  catch
  {
     throw;
  }
   }
}

дает следующий результат:

$ /cygdrive/c/Windows/Microsoft.NET/Framework/v4.0.30319/csc.exe Test.cs
Microsoft (R) Visual C# 2010 Compiler version 4.0.30319.1
Copyright (C) Microsoft Corporation. All rights reserved.

$ ./Test.exe
First test
   at Ex.ThrowWithVariable()
   at Ex.Main()
Second test
   at Ex.ThrowWithoutVariable()
   at Ex.Main()

что полностью противоречит сообщению в блоге.

Такой же результат получается с помощью кода из: http://crazorsharp.blogspot.com/2009/08/rethrowing-exception-without-resetting.html

Оригинальный вопрос: что я делаю не так?

ОБНОВЛЕНИЕ: тот же результат с .Net 3.5/csc.exe 3.5.30729.4926

SUMUP: все ваши ответы были великолепны, еще раз спасибо.

Таким образом, причина заключается в том, что 64-битный JITter эффективно встроен.

Мне пришлось выбрать только один ответ, и вот почему я выбрал ответ LukeH:

  • он догадался о встраиваемой проблеме и о том, что она может быть связана с моей 64-битной архитектурой,

  • он предоставил флаг NoInlining, который является самым простым способом избежать такого поведения.

Однако теперь эта проблема поднимает другой вопрос: соответствует ли это поведение всем спецификациям .Net: CLR и языкам программирования С#?

ОБНОВЛЕНИЕ: эта оптимизация кажется совместимой в соответствии с: Throw VS rethrow: тот же результат? (спасибо 0xA3)

Заранее спасибо за вашу помощь.

Ответ 1

Я не могу воспроизвести проблему - использование .NET 3.5 (32-разрядная версия) дает мне те же результаты, что описаны в статье Bart.

Я предполагаю, что компилятор/джиттер .NET 4 - или, может быть, 64-битный компилятор/джиттер, если это происходит и в версии 3.5, - внедряет метод BadGuy в вызывающие методы. Попробуйте добавить следующий атрибут MethodImpl в BadGuy и посмотрите, имеет ли это значение:

[MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)]
private static void BadGuy()
{
    //
    // Some nasty behavior.
    //
    throw new Exception();
}

Ответ 2

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

Я подозреваю, что происходит то, что встраивание компилятора просто заменило вызов BadGuy() на throw new Exception();, потому что это единственный оператор в BadGuy().

Если вы отключите опцию "Оптимизировать код" в свойствах проекта → экран "Сборка", то сборка "Выпуск" и "Отладка" приведут к тому же результату, который показывает BadGuy() в верхней части трассировки стека.

Ответ 3

Кажется, что JIT-оптимизаторы здесь работают. Как вы можете видеть, стек вызовов во втором случае отличается от первого случая, когда вы запускаете сборку Debug. Однако в сборке Release оба стека вызовов идентичны из-за оптимизации.

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

[MethodImpl(MethodImplOptions.NoOptimization)]
private static void ThrowWithoutVariable()
{
    try
    {
        BadGuy();
    }
    catch
    {
        throw;
    }
}

Обратите внимание, что IL по-прежнему отличается для ThrowWithoutVariable и ThrowWithVariable:

.method private hidebysig static void  ThrowWithVariable() cil managed
{
  // Code size       11 (0xb)
  .maxstack  1
  .locals init ([0] class [mscorlib]System.Exception ex)
  .try
  {
    IL_0000:  call       void Ex::BadGuy()
    IL_0005:  leave.s    IL_000a
  }  // end .try
  catch [mscorlib]System.Exception 
  {
    IL_0007:  stloc.0
    IL_0008:  ldloc.0
    IL_0009:  throw
  }  // end handler
  IL_000a:  ret
} // end of method Ex::ThrowWithVariable

.method private hidebysig static void  ThrowWithoutVariable() cil managed
{
  // Code size       11 (0xb)
  .maxstack  1
  .try
  {
    IL_0000:  call       void Ex::BadGuy()
    IL_0005:  leave.s    IL_000a
  }  // end .try
  catch [mscorlib]System.Object 
  {
    IL_0007:  pop
    IL_0008:  rethrow
  }  // end handler
  IL_000a:  ret
} // end of method Ex::ThrowWithoutVariable

Обновите, чтобы ответить на дополнительный вопрос, соответствует ли это спецификации CLI

Фактически это соответствует, а именно, чтобы позволить JIT-компилятору включить важные оптимизации. Приложение F утверждает на странице 52 (выделено мной):

Некоторые инструкции CIL выполняют неявно проверки во время выполнения, которые обеспечивают память и Тип безопасности. Первоначально, CLI гарантировано, что исключения были точный, что означает, что состояние программы был сохранен, когда исключение было выброшены. Тем не менее, соблюдение точного исключения для неявных проверок делает некоторые важные оптимизации практически невозможно применить. Программисты теперь могут объявить через пользовательский атрибут, что метод "расслабленный", который говорит, что исключения в результате неявных проверок во время выполнения не должно быть точным.

Расслабленные чеки сохранить проверяемость (сохраняя память и безопасность типов) пока разрешить оптимизацию, которая переупорядочить инструкции. В частности, это включает следующие оптимизации:

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

Ответ 4

Используйте отладочную сборку, и вы увидите разницу более четко. При отладочной сборке первый запуск покажет местоположение в виде строки throw ex, а второй - как исходный от фактического вызова BadGuy. Очевидно, что "проблема" - это вызов BadGuy, а не бросок ex line, и вы будете преследовать меньше spectreов с помощью прямого оператора throw;.

В трассировке стека это мелкое преимущество не столь очевидно, в очень глубоком стеке вы замаскируете реальный источник проблемы и потеряете некоторую точность, выбрасывая исключение вручную вместо использования встроенного оператора re-throw.

Ответ 5

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

    //This terrible hack makes sure track trace is preserved if exception is re-thrown
    internal static Exception AppendStackTrace(Exception ex)
    {
        //Fool CLR into appending stack trace information when the exception is re-thrown
        var remoteStackTraceString = typeof(Exception).GetField("_remoteStackTraceString",
                                                                 BindingFlags.Instance |
                                                                 BindingFlags.NonPublic);
        if (remoteStackTraceString != null)
            remoteStackTraceString.SetValue(ex, ex.StackTrace + Environment.NewLine);

        return ex;
    }