Стоимость блокировки в .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?
Ответы
Ответ 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 сделает это.
См. эту очень информативную статью, особенно часть, посвященную проблеме блокировки.