Барьеры памяти при входе и выходе из Java синхронизированного блока - PullRequest
2 голосов
/ 29 мая 2020

Я нашел ответы здесь, на SO, о Java сбрасывании рабочей копии переменных в синхронизированном блоке во время выхода. Точно так же он синхронизирует все переменные из основной памяти один раз во время входа в синхронизированный раздел.

Однако у меня есть несколько фундаментальных вопросов по этому поводу: в основном энергонезависимые переменные экземпляра в моем синхронизированном разделе? Будет ли JVM автоматически кэшировать эти переменные в регистры ЦП во время входа в блок, а затем выполнять все необходимые вычисления, прежде чем окончательно сбросить их обратно?

У меня есть синхронизированный блок как ниже: подчеркнутые переменные _ например _callStartsInLastSecondTracker - это все переменные экземпляра, к которым я часто обращаюсь в этом критическом разделе.

public CallCompletion startCall()
{
  long currentTime;
  Pending pending;
  synchronized (_lock)
  {
    currentTime = _clock.currentTimeMillis();
    _tracker.getStatsWithCurrentTime(currentTime);
    _callStartCountTotal++;
    _tracker._callStartCount++;
    if (_callStartsInLastSecondTracker != null)
      _callStartsInLastSecondTracker.addCall();
    _concurrency++;
    if (_concurrency > _tracker._concurrentMax) 
    { 
      _tracker._concurrentMax = _concurrency;
    }
    _lastStartTime = currentTime;
    _sumOfOutstandingStartTimes += currentTime;
    pending = checkForPending();
  }
  if (pending != null) 
  {
    pending.deliver();
  }
  return new CallCompletionImpl(currentTime);
}

Означает ли это, что все эти операции, например +=, ++, > et c. требует, чтобы JVM постоянно взаимодействовала с основной памятью? Если да, могу ли я использовать локальные переменные для их кеширования (желательно выделения стека для примитивов) и выполнять операции и в конце назначать их обратно переменным экземпляра? Поможет ли это оптимизировать производительность этого блока?

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

Благодарю за любую помощь здесь.

Ответы [ 2 ]

2 голосов
/ 29 мая 2020

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

Я бы предположил, что JVM может и будет оптимизировать их в регистры, если вы многократно обращаетесь к ним внутри того же блока synchronized.

т.е. открытие { и закрытие } - это барьеры памяти ( получение и снятие блокировки), но внутри этого блока применяются обычные правила.

Обычные правила для переменных, отличных от volatile, похожи на в C ++: JIT-компилятор может хранить частные копии / временные файлы и выполнять полную оптимизацию. Закрытие } делает все назначения видимыми перед тем, как пометить блокировку как освобожденную, поэтому любой другой поток, выполняющий тот же синхронизированный блок, увидит эти изменения.

Но если вы читаете / записываете эти переменные снаружи a synchronized(_lock), пока выполняется этот synchronized блок, нет никакой гарантии упорядочивания и только те гарантии атомарности, которые есть у Java. Только volatile заставит JVM перечитывать переменную при каждом доступе.


большую часть времени потоки находятся в состоянии WAITING, и пропускная способность также очень низкая. Отсюда необходимость оптимизации.

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

Но не было бы достаточно большого эффекта, чтобы заставить большинство потоков блокироваться в ожидании блокировок (или I / O?) Большую часть времени, по сравнению с тем, что большую часть времени активно работает большинство потоков.

Комментарий @ Kayaman, скорее всего, правильный: это проблема дизайна, слишком много работы выполняется внутри одного большого мьютекса . Я не вижу циклов внутри вашего критического раздела, но, по-видимому, некоторые из тех методов, которые вы вызываете, содержат циклы или являются дорогостоящими, и никакой другой поток не может войти в этот блок synchronized(_lock), пока в нем находится один поток.


Теоретическое замедление в худшем случае для сохранения / перезагрузки из памяти (например, компиляция C в антиоптимизированном режиме отладки) по сравнению с сохранением переменной в регистре будет для чего-то вроде while (--shared_var >= 0) {}, что даст, возможно, 6-кратное замедление на текущее оборудование x86. (Задержка в 1 цикл для dec eax по сравнению с этой задержкой плюс 5 циклов пересылки хранилища для адресата памяти dec). Но это только в том случае, если вы зацикливаете общую переменную или иным образом создаете цепочку зависимостей путем ее повторной модификации.

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

В гораздо более вероятном случае кода, который просто читает переменную несколько раз, антиоптимизированный код, который действительно загружается каждый раз, может очень эффективно воздействовать на все эти нагрузки в кеш L1d. На x86 вы, вероятно, вряд ли заметите разницу, поскольку современные процессоры имеют пропускную способность нагрузки 2 / такт и эффективную обработку инструкций ALU с операндами источника памяти, например, cmp eax, [rdi] в основном так же эффективны, как cmp eax, edx.

(ЦП имеют согласованные кеши, поэтому нет необходимости сбрасывать или полностью переходить в DRAM, чтобы гарантировать, что вы «видите» данные из других ядер; компилятор JVM или C должен только убедиться, что загрузка или сохранение действительно происходит в asm , не оптимизированы в регистр. Регистры являются частными для потоков.)

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

1 голос
/ 29 мая 2020

Вы обращаетесь к членам одного объекта. Поэтому, когда ЦП считывает член _lock, ему необходимо сначала загрузить строку кэша, содержащую член _lock. Так что, вероятно, довольно много переменных-членов будут в той же строке кэша, которая уже находится в вашем кеше.

Я бы больше беспокоился о самом синхронизированном блоке, ЕСЛИ вы определили, что это действительно проблема; это может быть совсем не проблема. Например, Java использует довольно много методов оптимизации блокировок, таких как смещенная блокировка, адаптивная спин-блокировка, чтобы снизить стоимость блокировок.

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

Я бы ни на секунду не доверял JPofiler. http://psy-lob-saw.blogspot.com/2016/02/why-most-sampling-java-profilers-are.html Так что, возможно, JProfiler направляет вас в неправильном направлении.

...