2009-08-13 2 views
4

Рассмотрим следующий код:Использует делегатов чрезмерно плохой идеей для производительности?

if (IsDebuggingEnabled) { 
    instance.Log(GetDetailedDebugInfo()); 
} 

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

Теперь, уборщик альтернатива закодировать что-то вроде этого:

instance.Log(() => GetDetailedDebugInfo()); 

Где .log() определяется как:

public void Log(Func<string> getMessage) 
{ 
    if (IsDebuggingEnabled) 
    { 
     LogInternal(getMessage.Invoke()); 
    } 
} 

Моя забота с производительностью, предварительное тестирование Безразлично» t показывает, что второй случай будет особенно дорогим, но я не хочу сталкиваться с неожиданностями при увеличении нагрузки.

О, и, пожалуйста, не предлагайте условную компиляцию, потому что это не относится к этому случаю.

(PS: Я написал код прямо в StackOverflow Задать вопрос текстовое поле, так что не обессудьте, если есть тонкие ошибки и не компилирует, вы получите точку :)

+0

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

ответ

3

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

Так что я написал простой тест:

public static bool IsDebuggingEnabled { get; set; } 


    static void Main(string[] args) 
    { 
     for (int j = 0; j <= 10; j++) 
     { 
      Stopwatch sw = Stopwatch.StartNew(); 
      for (int i = 0; i <= 15000; i++) 
      { 
       Log(GetDebugMessage); 
       if (i % 1000 == 0) IsDebuggingEnabled = !IsDebuggingEnabled; 
      } 
      sw.Stop(); 
      Console.WriteLine(sw.ElapsedMilliseconds); 
     } 

     Console.ReadLine(); 
     for (int j = 0; j <= 10; j++) 
     { 
      Stopwatch sw = Stopwatch.StartNew(); 
      for (int i = 0; i <= 15000; i++) 
      { 
       if (IsDebuggingEnabled) GetDebugMessage(); 
       if (i % 1000 == 0) IsDebuggingEnabled = !IsDebuggingEnabled; 
      } 
      sw.Stop(); 
      Console.WriteLine(sw.ElapsedMilliseconds); 
     } 
     Console.ReadLine(); 
    } 

    public static string GetDebugMessage() 
    { 
     StringBuilder sb = new StringBuilder(100); 
     Random rnd = new Random(); 
     for (int i = 0; i < 100; i++) 
     { 
      sb.Append(rnd.Next(100, 150)); 
     } 
     return sb.ToString(); 
    } 

    public static void Log(Func<string> getMessage) 
    { 
     if (IsDebuggingEnabled) 
     { 
      getMessage(); 
     } 
    } 

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

Похоже, я ответил на свой вопрос.

0

вызова GetMessage Делегат вместо вызова Invoke на него.

if(IsDebuggingEnabled) 
{ 
    LogInternal(getMessage()); 
} 

Вы также должны добавить null check on getMessage.

+0

Мой первоначальный комментарий включал неправильный оператор, говорящий, что вызов делегата напрямую, а не вызов Invoke на нем, будет быстрее. Однако я ошибся, и я удалил этот комментарий. Я написал небольшое консольное приложение, в котором я вызывал делегата типа Func как напрямую, так и вызывая Invoke на нем. Затем я посмотрел на сгенерированный IL с помощью инструмента ildasm, и IL был одинаковым для обоих вызовов: callvirt instance! 0 class [System.Core] System.Func'1 :: Invoke() –

-3

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

Подвешивание! Я исправлен! Делегаты используют только потоки, когда вы используете «BeginInvoke» ... поэтому мои приведенные выше комментарии не применяются к тому, как вы их используете.

+10

Ваша вера должна быть пересмотрена. Делегаты полностью не связаны с потоками. –

+3

Делегаты * могут использоваться *, чтобы легко выполнять несколько многопоточных операций. Возможно, именно здесь возникает путаница. –

5

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

Обратите внимание, что если вы хотите сделать это в сборках Debug, вы можете добавить атрибут [Conditional("DEBUG")] к методу журнала.

+0

Er, а как насчет того, что лямбда, вызываемая или нет, захватывает переменные и, следовательно, заставляет их подниматься из стека на кучу? Это может существенно повлиять на перфект, так как JIT значительно отличается от значений полей кеширования в регистрах, поэтому, если переменная захватывается, возможно, будут загружены дополнительные объемы и магазины памяти, и вы действительно заметите разницу в жестком цикле. –

+1

@Pavel: В общем, это правда. Но для цели ОП нет захваченной переменной. –

+0

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

1

Вы также можете сделать это:

// no need for a lambda 
instance.Log(GetDetailedDebugInfo) 

// Using these instance methods on the logger 
public void Log(Func<string> detailsProvider) 
{ 
    if (!DebuggingEnabled) 
     return; 

    this.LogImpl(detailsProvider()); 
} 

public void Log(string message) 
{ 
    if (!DebuggingEnabled) 
     return; 

    this.LogImpl(message); 
} 

protected virtual void LogImpl(string message) 
{ 
    .... 
} 
0

Стандартные ответы:

  • Если вам нужно сделать это, ты должен это сделать.
  • Заверните его 10^9 раз, посмотрите на секундомер, &, который расскажет вам, сколько наносекунд требуется.
  • Если ваша программа большая, возможно, у вас больше проблем в другом месте.
3

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

Сказав, что для первого примера:

if (IsDebuggingEnabled) 
{ 
    instance.Log(GetDetailedDebugInfo()); 
} 

Если IsDebuggingEnabled является статическим, то только для чтения чек будет JITted прочь, как он знает, что он никогда не может измениться. Это означает, что приведенный выше пример будет иметь нулевое влияние на производительность, если IsDebuggingEnabled является ложным, потому что после завершения JIT код будет удален.

instance.Log(() => GetDetailedDebugInfo()); 

public void Log(Func<string> getMessage) 
{ 
    if (IsDebuggingEnabled) 
    { 
     LogInternal(getMessage.Invoke()); 
    } 
} 

Метод будет вызываться каждый раз после экземпляра instance.Log. Который будет медленнее.

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

Смежные вопросы