Я тестировал, как Interlocked.Increment и lock ведут себя на архитектуре моего компьютера, потому что я прочитал следующие строки в в этой статье.

Переписанный с помощью Interlocked.Increment, метод должен выполняться быстрее, по крайней мере, на некоторых архитектурах.

Используя следующий код, я убеждаюсь, что в моих проектах стоит проверить блокировки.

var watch = new Stopwatch();
var locker = new object();
int counter = 0;

watch.Start();
for (int i = 0; i < 100000000; i++)
{
    lock (locker)
    {
        counter++;
    }
}
watch.Stop();
Console.WriteLine(watch.Elapsed.TotalSeconds);

watch.Reset();
counter = 0;

watch.Start();
for (int i = 0; i < 100000000; i++)
{
    Interlocked.Increment(ref counter);
}
watch.Stop();
Console.WriteLine(watch.Elapsed.TotalSeconds);

Я получаю стабильные результаты с приблизительными значениями 2,4 с для блокировки и 1,2 с для блокировки. Однако я был удивлен, обнаружив, что запуск этого кода в режиме выпуска улучшает значение только для Interlocked примерно до 0,7 с , а время блокировки остается прежним. Это почему? Как оптимизируется Interlocked, когда в режиме выпуска блокировки нет?

3
Ondrej Janacek 22 Дек 2013 в 18:01
1
Как правило, все измерения производительности следует проводить только в режиме Release. Измерения производительности режима отладки не имеют значения. Прочтите MSIL, предоставленный компилятором C #, возможно, код слишком оптимизирован (например, Interlocked заменен на ++).
 – 
Alex F
22 Дек 2013 в 18:07
1
Для неконтролируемой области блокировки требуются две взаимосвязанные инструкции. Измеренные вами числа прекрасно это показывают, по крайней мере, в режиме отладки. Кстати, ваш тест очень мало значит, потому что стоимость синхронизации зависит от блокировки и конкуренции строк кеша. Ваш тест предполагает, что их нет.
 – 
usr
22 Дек 2013 в 18:10
Было бы здорово, если бы вы могли написать об этом больше и опубликовать в качестве ответа. Я не понимаю MSIL, как предлагает Алекс, и, поскольку я прочитал всю статью, а этого я все еще не понимаю полностью, хотелось бы получить дополнительную информацию по этой теме.
 – 
Ondrej Janacek
22 Дек 2013 в 18:16
У меня нет ответа, потому что я не знаю, почему циклы ведут себя по-разному в режиме Release. Надо бы посмотреть на разборке. У меня сейчас нет времени на это расследование.
 – 
usr
22 Дек 2013 в 18:18
2
Тестирование производительности в режиме отладки - бессмысленная трата времени; Ни одно из полученных вами чисел не будет иметь даже отдаленного смысла - если, конечно, ваши клиенты не собираются запускать вашу программу в режиме отладки.
 – 
Eric Lippert
22 Дек 2013 в 20:04

1 ответ

Лучший ответ

Вы должны посмотреть на сгенерированный машинный код, чтобы увидеть разницу: Debug + Windows + Disassembly. Версия отладочной сборки вызова Interlocked.Increment ():

   00FC27AD  call        7327A810 

Версия сборки релиза:

   025F279D  lock inc    dword ptr [ebp-24h] 

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

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

5
Hans Passant 22 Дек 2013 в 20:22