Летучий чи дорогий?


111

Після прочитання кулінарної книги JSR-133 для письменників-компіляторів про реалізацію енергонезалежних, особливо в розділі "Взаємодія з атомними інструкціями", я вважаю, що для читання летючої змінної без оновлення вона потребує LoadLoad або LoadStore бар'єр. Далі на сторінці я бачу, що LoadLoad та LoadStore фактично не працюють на процесорах X86. Чи означає це, що оперативні операції зчитування можуть бути виконані без явного відключення кеша на x86, і це так само швидко, як і звичайна змінна зчитування (без урахування обмежувальних обмежень на летючі)?

Я вважаю, що я не розумію цього правильно. Може хтось не піклується просвітити мене?

EDIT: Цікаво, чи є відмінності в багатопроцесорних середовищах. На одних системах процесора процесор може дивитись на власні кеші потоків, як стверджує Джон В., але в декількох системах процесора має бути певна опція конфігурації процесорів, що цього недостатньо, і головна пам'ять повинна бути відбита, роблячи мінливою повільніше на кількох процесорних системах, правда?

PS: На моєму шляху дізнатися більше про це я натрапив на наступні чудові статті, і оскільки це питання може бути цікавим іншим, я поділюсь своїми посиланнями тут:


1
Ви можете прочитати мою редакцію про конфігурацію з декількома процесорами, на які ви посилаєтесь. Може трапитися так, що для систем з декількома процесорами для короткочасного використання не більше одного читання / запису в основну пам'ять.
Джон Вінт

2
самостійке читання не є дорогим. основна вартість полягає в тому, як це запобігає оптимізації. на практиці ціна в середньому також не дуже висока, якщо тільки в енергійному циклі не використовується енергонезалежний.
незаперечний

2
Ця стаття про infoq ( infoq.com/articles/memory_barriers_jvm_concurrency ) також може вас зацікавити, вона показує ефекти летючих і синхронізованих на створений код для різних архітектур. Це також один випадок, коли jvm може працювати краще, ніж достроковий компілятор, оскільки він знає, чи працює він в однопроцесорній системі і може опускати деякі бар'єри пам'яті.
Йорн Хорстманн

Відповіді:


123

В Intel безперервне непостійне читання коштує досить дешево. Якщо ми розглянемо такий простий випадок:

public static long l;

public static void run() {        
    if (l == -1)
        System.exit(-1);

    if (l == -2)
        System.exit(-1);
}

Використання можливості Java 7 для друку асемблерного коду метод запуску виглядає приблизно так:

# {method} 'run2' '()V' in 'Test2'
#           [sp+0x10]  (sp of caller)
0xb396ce80: mov    %eax,-0x3000(%esp)
0xb396ce87: push   %ebp
0xb396ce88: sub    $0x8,%esp          ;*synchronization entry
                                    ; - Test2::run2@-1 (line 33)
0xb396ce8e: mov    $0xffffffff,%ecx
0xb396ce93: mov    $0xffffffff,%ebx
0xb396ce98: mov    $0x6fa2b2f0,%esi   ;   {oop('Test2')}
0xb396ce9d: mov    0x150(%esi),%ebp
0xb396cea3: mov    0x154(%esi),%edi   ;*getstatic l
                                    ; - Test2::run@0 (line 33)
0xb396cea9: cmp    %ecx,%ebp
0xb396ceab: jne    0xb396ceaf
0xb396cead: cmp    %ebx,%edi
0xb396ceaf: je     0xb396cece         ;*getstatic l
                                    ; - Test2::run@14 (line 37)
0xb396ceb1: mov    $0xfffffffe,%ecx
0xb396ceb6: mov    $0xffffffff,%ebx
0xb396cebb: cmp    %ecx,%ebp
0xb396cebd: jne    0xb396cec1
0xb396cebf: cmp    %ebx,%edi
0xb396cec1: je     0xb396ceeb         ;*return
                                    ; - Test2::run@28 (line 40)
0xb396cec3: add    $0x8,%esp
0xb396cec6: pop    %ebp
0xb396cec7: test   %eax,0xb7732000    ;   {poll_return}
;... lines removed

Якщо ви подивитесь на 2 посилання на getstatic, перша включає завантаження з пам'яті, друга пропускає навантаження, оскільки значення повторно використовується з реєстру (ів), в яке він уже завантажений (довгий - 64 біт і на моєму 32-бітовому ноутбуці) він використовує 2 регістри).

Якщо ми зробимо змінну l летючою, отримана збірка буде іншою.

# {method} 'run2' '()V' in 'Test2'
#           [sp+0x10]  (sp of caller)
0xb3ab9340: mov    %eax,-0x3000(%esp)
0xb3ab9347: push   %ebp
0xb3ab9348: sub    $0x8,%esp          ;*synchronization entry
                                    ; - Test2::run2@-1 (line 32)
0xb3ab934e: mov    $0xffffffff,%ecx
0xb3ab9353: mov    $0xffffffff,%ebx
0xb3ab9358: mov    $0x150,%ebp
0xb3ab935d: movsd  0x6fb7b2f0(%ebp),%xmm0  ;   {oop('Test2')}
0xb3ab9365: movd   %xmm0,%eax
0xb3ab9369: psrlq  $0x20,%xmm0
0xb3ab936e: movd   %xmm0,%edx         ;*getstatic l
                                    ; - Test2::run@0 (line 32)
0xb3ab9372: cmp    %ecx,%eax
0xb3ab9374: jne    0xb3ab9378
0xb3ab9376: cmp    %ebx,%edx
0xb3ab9378: je     0xb3ab93ac
0xb3ab937a: mov    $0xfffffffe,%ecx
0xb3ab937f: mov    $0xffffffff,%ebx
0xb3ab9384: movsd  0x6fb7b2f0(%ebp),%xmm0  ;   {oop('Test2')}
0xb3ab938c: movd   %xmm0,%ebp
0xb3ab9390: psrlq  $0x20,%xmm0
0xb3ab9395: movd   %xmm0,%edi         ;*getstatic l
                                    ; - Test2::run@14 (line 36)
0xb3ab9399: cmp    %ecx,%ebp
0xb3ab939b: jne    0xb3ab939f
0xb3ab939d: cmp    %ebx,%edi
0xb3ab939f: je     0xb3ab93ba         ;*return
;... lines removed

У цьому випадку обидві гестатичні посилання на змінну l передбачають навантаження з пам'яті, тобто значення не може зберігатися в регістрі через кілька змінних зчитувань. Для забезпечення атомного зчитування значення зчитується з основної пам’яті в регістр MMX, що movsd 0x6fb7b2f0(%ebp),%xmm0робить операцію зчитування єдиною інструкцією (з попереднього прикладу ми бачили, що для 32-бітового значення зазвичай потрібно 32-бітових зчитування в 32-бітовій системі).

Таким чином, загальна вартість енергонезалежного зчитування буде приблизно еквівалентна навантаженню пам'яті і може бути такою ж дешевою, як доступ до кешу L1. Однак якщо інше ядро ​​записує на змінну змінну, кеш-рядок буде недійсним, вимагаючи основної пам'яті або, можливо, доступу до кешу L3. Фактична вартість буде сильно залежати від архітектури процесора. Навіть між Intel та AMD протоколи когерентності кеш-пам'яті різні.


бічна зауваження, java 6 має таку ж здатність показувати збірку (це
точкова

+1 у JDK5 непостійний не може бути переупорядкований стосовно будь-якого читання / запису (що, наприклад, фіксує блокування подвійної перевірки) Чи означає це, що це також вплине на те, як маніпулюють енергонезалежними полями? Було б цікаво змішати доступ до енергонезалежних та енергонезалежних полів.
ewernli

@evemli, вам потрібно бути обережним, я сам робив це твердження один раз, але було виявлено, що воно є невірним. Є крайовий корпус. Модель пам'яті Java дозволяє проводити семантику мотеля плотви, коли магазини можна переупорядкувати попереду енергонезалежних магазинів. Якщо ви вибрали це із статті Брайана Геца на сайті IBM, то варто згадати, що ця стаття спрощує специфікацію JMM.
Майкл Баркер

20

Взагалі кажучи, на більшості сучасних процесорів летючі навантаження порівнянні з нормальним навантаженням. Летючий склад - це приблизно 1/3 часу моніторингу-входу / монітора-виходу. Це видно в системах, кешованих кешем.

Щоб відповісти на питання ОП, мінливі записи коштують дорого, а читання зазвичай - ні.

Чи означає це, що оперативні операції зчитування можуть бути виконані без явного відключення кеша на x86, і це так само швидко, як і звичайна змінна зчитування (нехтуючи перепорядковуючими контрацептами летючих)?

Так, іноді під час перевірки поля процесор може навіть не потрапляти в основну пам'ять, натомість шпигує за іншими кешами потоків і отримує звідти значення (дуже загальне пояснення).

Однак я зазначаю пропозицію Ніла про те, що якщо у вас є поле, до якого звертаються декілька потоків, ви повинні обернути його як AtomicReference. Будучи AtomicReference, він виконує приблизно таку ж пропускну здатність для читання / запису, але також очевидно, що поле буде доступне та змінено декількома потоками.

Змінити, щоб відповісти на редагування ОП:

Злагодженість кешу - це трохи складний протокол, але коротше: процесори поділять загальну лінію кешу, яка приєднана до основної пам'яті. Якщо процесор завантажує пам'ять, а жоден інший процесор не мав цього, ЦП вважає, що це найсучасніше значення. Якщо інший процесор намагається завантажити те саме місце пам'яті, то вже завантажений процесор буде про це знати і фактично поділиться кешованим посиланням на запитуючий процесор - тепер ЦП запиту має копію цієї пам'яті у своєму кеш-процесорі. (Ніколи не доводилося шукати посилання в основній пам'яті)

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


5
AtomicReference - це лише обгортка мінливого поля з доданими нативних функцій, що надають додаткові функціональні можливості, такі як getAndSet, сравнениеAndSet тощо, тому з точки зору продуктивності його використання просто корисно, якщо вам потрібна додаткова функціональність. Але мені цікаво, чому ви тут звертаєтесь до ОС? Функціональність реалізована безпосередньо в опкодах CPU. І чи означає це, що в багатопроцесорних системах, де один процесор не знає про вміст кешу інших процесорів, летючі речовини повільніші, оскільки процесори завжди повинні потрапляти в основну пам'ять?
Даніель

Ти маєш рацію, я сумую, говорив про ОС, що повинен писати ЦП, виправляючи це зараз. І так, я знаю, що AtomicReference - це просто обгортка для мінливих полів, але вона також додає як своєрідну документацію, що до самого поля буде доступ з декількох потоків.
Джон Вінт

@John, чому б ти додав ще одну непряму за допомогою AtomicReference? Якщо вам потрібен CAS - добре, але AtomicUpdater може бути кращим варіантом. Наскільки я пам’ятаю, немає жодних суті про AtomicReference.
bestsss

@bestsss Для всіх загальних приміщень ви праві, немає різниці між AtomicReference.set / get і летючим завантаженням і магазинами. Це, як сказано, у мене було те саме (і в деякій мірі) щодо того, коли їх використовувати. Ця відповідь може трохи деталізувати її stackoverflow.com/questions/3964317/… . Використання будь-якого є більш перевагою, мій єдиний аргумент для використання AtomicReference над простою мінливою - це чітка документація - що саме по собі не дає найбільшого аргументу, як я розумію
Джон Вінт

Зі сторони, деякі аргументи, що використовують мінливе поле / AtomicReference (без необхідності використання CAS) призводять до помилкового коду old.nabble.com/…
Джон Vint,

12

Словами моделі пам'яті Java (як визначено для Java 5+ у JSR 133), будь-яка операція - читання або запис - на volatileзмінній створює зв'язок " раніше" стосовно будь-якої іншої операції на тій самій змінній. Це означає, що компілятор і JIT змушені уникати певних оптимізацій, таких як переупорядкування інструкцій всередині потоку або виконання операцій лише в локальному кеші.

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

Тим не менш, ви не повинні робити змінну, volatileякщо ви не знаєте, що до неї можна отримати доступ з декількох потоків поза synchronizedблоками. Навіть тоді ви повинні розглянути питання про те летючий є кращим вибором по порівнянні з synchronized, AtomicReferenceі його друзі, явні Lockкласи і т.д.


4

Доступ до змінної змінної багато в чому схожий із завершенням доступу до звичайної змінної в синхронізованому блоці. Наприклад, доступ до мінливої ​​змінної заважає ЦП повторно замовляти інструкції до і після доступу, і це, як правило, уповільнює виконання (хоча я не можу сказати на скільки).

Більш загально, в багатопроцесорній системі я не бачу, як доступ до летючої змінної можна зробити без штрафних санкцій - повинен бути певний спосіб забезпечити синхронізацію запису на процесорі A з читанням на процесорі B.


4
Читання мінливих змінних має такий же штраф, як і введення монітора, щодо можливостей впорядкування вказівок, тоді як запис змінної змінної дорівнює монітору-виходу. Різниця може полягати в тому, які змінні (наприклад, кеші процесора) можуть бути очищені або недійсні. У той час як синхронізовано промиває або визнає недійсним все, доступ до змінної змінної завжди повинен ігнорувати кеш-пам'ять.
Даніель

12
-1, Доступ до змінної змінної є дещо іншим, ніж використання синхронізованого блоку. Для введення синхронізованого блоку потрібне атомне записування на основі сравнениеAndSet, щоб вийняти замок і летючу запис, щоб звільнити його. Якщо блокування задоволено, то керування має перейти від простору користувача до простору ядра для арбітражу блокування (це дорогий біт). Доступ до енергонезалежних завжди залишатиметься у просторі користувача.
Майкл Баркер

@MichaelBarker: Ви впевнені, що всі монітори повинні охороняти ядро, а не додаток?
Даніель

@Daniel: Якщо ви представляєте монітор за допомогою синхронізованого блоку або блокування, то так, але лише у разі задоволення монітора. Єдиний спосіб зробити це без арбітражу ядра - використовувати ту саму логіку, але зайнятий спін замість паркування нитки.
Майкл Баркер

@MichaelBarker: Гаразд, щодо задоволених замків я це розумію.
Даніель
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.