Подтвердить что ты не робот

Стоимость блокировки в .NET vs Java

Я играл с 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();

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

4b9b3361

Ответ 1

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

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

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

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

Ответ 2

Первое, что нужно помнить о микро-тестах, - это то, что 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

Является ли переменная '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 может помочь пролить свет.

Ответ 4

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

В Java я сравнивал его с симулированным имплантом в незащищенном случае

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 также близок к минимуму, он, по-видимому, делает одну или две вещи больше, чем Java. Я не знаком с С#, но это, вероятно, указывает на некоторую разницу в семантике, поэтому С# должен сделать что-то дополнительное.

Ответ 5

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

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

Ответ 6

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

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