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

Ссылаясь на большое количество документации в сети, особенно на SO, например: Каков правильный способ перезапустить исключение в C#? должна быть разница между "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 и с языком программирования C#?

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

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

5 ответов

Решение

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

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

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

Похоже, что 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 гарантировал, что исключения были точными, а это означает, что состояние программы сохранялось при возникновении исключения. Однако применение точных исключений для неявных проверок делает некоторые важные оптимизации практически невозможными для применения. Программисты могут теперь объявить через собственный атрибут, что метод "расслаблен", что говорит о том, что исключения, возникающие из неявных проверок во время выполнения, не обязательно должны быть точными.

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

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

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

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

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

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

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

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

    //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;
    }
Другие вопросы по тегам