Стоимость блокировки в .NET против Java - PullRequest
28 голосов
/ 27 августа 2011

Я играл с Disruptor framework и его портом для платформы .NET и нашел интересный случай. Может быть, я что-то пропустил, поэтому я ищу помощи от всемогущего сообщества .

        long iterations = 500*1000*1000;
        long testValue = 1;

        //.NET 4.0. Release build. Mean time - 26 secs;
        object lockObject = new object();
        Stopwatch sw = Stopwatch.StartNew();
        for (int i = 0; i < iterations; i++)
        {
            lock (lockObject)
            {
                testValue++;    
            }
        }
        sw.Stop();

        //Java 6.25. Default JVM params. Mean time - 17 secs.
        Object lock = new Object();
        long start = System.currentTimeMillis();
        for (int i = 0; i < iterations; i++)
        {
                synchronized (lock)
                {
                    testValue++;
                }
        }
        long stop = System.currentTimeMillis();

Кажется, что получение блокировки в сценарии с потоком подписи в .NET стоит всего на 50% больше, чем в Java. Сначала я относился к таймерам с подозрением, но несколько раз проводил один и тот же тест, результаты которого были примерно выше упомянутых выше средних значений. Тогда я с подозрением отнесся к синхронизированному блоку кода, но это не более, чем просто monitorenter / monitorexit инструкции байт-кода - то же самое, что и lock ключевое слово в .NET. Любые другие идеи, почему взлом блокировки так дорог в .NET против Java?

Ответы [ 6 ]

22 голосов
/ 27 августа 2011

Да, похоже, что взломать бесконтрольную блокировку дороже в .NET, чем в Java. (Результаты на моем нетбуке немного более драматичны.)

Существуют различные аспекты производительности, которые будут быстрее на одной платформе, чем на другой, иногда до такой степени. HotSpot JIT и .NET JIT довольно сильно отличаются друг от друга по-разному - не в последнюю очередь потому, что .NET JIT запускается один раз на IL, тогда как HotSpot может оптимизировать все больше и больше как отдельный фрагмент кода запускается все чаще и чаще.

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

Недавно я обнаружил две ошибки .NET ( часть первая ; часть вторая ), с которыми мне приходится работать, когда я пишу «библиотеку системного уровня», и они имел бы существенное значение, если бы приложение выполняло лот анализа даты / времени - но такого рода микрооптимизация редко стоит делать.

8 голосов
/ 28 августа 2011

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

Если Java удивительно быстр по сравнению с другим языком, первый вопрос должен быть; Делает ли код что-нибудь полезное удаленно? (или даже похоже, что это может быть полезно)

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

for (int i = 0; i < iterations; i+=8) {
    synchronized (lock) {
        testValue++;
    }
    synchronized (lock) {
        testValue++;
    }
    synchronized (lock) {
        testValue++;
    }
    synchronized (lock) {
        testValue++;
    }
    synchronized (lock) {
        testValue++;
    }
    synchronized (lock) {
        testValue++;
    }
    synchronized (lock) {
        testValue++;
    }
    synchronized (lock) {
        testValue++;
    }
}

, который становится

for (int i = 0; i < iterations; i+=8) {
    synchronized (lock) {
        testValue++;
        testValue++;
        testValue++;
        testValue++;
        testValue++;
        testValue++;
        testValue++;
        testValue++;
    }
}

, поскольку testValue не используется.

for (int i = 0; i < iterations; i+=8) {
    synchronized (lock) {
    }
}

и, наконец,

{ }
3 голосов
/ 27 августа 2011

Является ли переменная testValue локальной для метода? Если это так, возможно, что JRE обнаружил, что блокировка не нужна, поскольку переменная является локальной для одного потока и, следовательно, не блокируется вообще.

Это объясняется здесь .

Чтобы показать, насколько сложно сказать, какую оптимизацию JVM решит сделать - и когда она решит это сделать - проанализируйте эти результаты при запуске кода три раза подряд:

public static void main(String[] args) {
  System.out.println("Java version: " + System.getProperty("java.version"));
  System.out.println("First call : " + doIt(500 * 1000 * 1000, 1)); // 14 secs
  System.out.println("Second call: " + doIt(500 * 1000 * 1000, 1)); // 1 sec
  System.out.println("Third call : " + doIt(500 * 1000 * 1000, 1)); // 0.4 secs
}

private static String doIt(final long iterations, long testValue) {
    Object lock = new Object();
    long start = System.currentTimeMillis();
    for (int i = 0; i < iterations; i++) {
        synchronized (lock) {
            testValue++;
        }
    }
    long stop = System.currentTimeMillis();
    return (stop - start) + " ms, result = " + testValue;
}

Эти результаты так сложно объяснить, я думаю, что только инженер JVM мог бы пролить свет.

1 голос
/ 30 августа 2011

Помните, оба очень быстры;мы говорим о 50 циклах ЦП для блокировки-чтения-записи-разблокировки.

В Java я сравнил его с имитацией impl в неконтролируемом случае

volatile int waitingList=0;

    AtomicInteger x = new AtomicInteger(0);
    for (int i = 0; i < iterations; i++)
    {
        while( ! x.compareAndSet(0, 1) )
            ;

        testValue++;

        if(waitingList!=0)
            ;
        x.set(0);
    }

немного быстрее, чем версия synchronized, затраченное время составляет 15 / 17.

Это показывает, что в вашем тестовом примере Java не делала сумасшедших оптимизаций, она честно делала lock-read-update-unlockкаждая итерация.Тем не менее, импл Java так же быстр, как имплант голой кости;это не может быть быстрее.

Хотя impl в C # также близок к минимуму, он, очевидно, делает на одну или две вещи больше, чем Java.Я не знаком с C #, но это, вероятно, указывает на некоторое различие в семантике, поэтому C # должен сделать что-то дополнительное.

0 голосов
/ 19 июня 2015

Java JIT оптимизирует синхронизацию, так как объект блокировки является локальным потоком (то есть он ограничен стеком потока и никогда не используется совместно) и, следовательно, никогда не сможет синхронизироваться из другого потока. Я не уверен, что .NET JIT сделает это.

См. эту очень информативную статью , особенно часть, посвященную выбору замка.

0 голосов
/ 12 сентября 2013

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

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

...