2015-10-11 2 views
4

У меня есть тест многопоточной JMH:Java безблокировочного производительность JMH

@State(Scope.Benchmark) 
@BenchmarkMode(Mode.Throughput) 
@OutputTimeUnit(TimeUnit.MICROSECONDS) 
@Fork(value = 1, jvmArgsAppend = { "-Xmx512m", "-server", "-XX:+AggressiveOpts","-XX:+UnlockDiagnosticVMOptions", 
     "-XX:+UnlockExperimentalVMOptions", "-XX:+PrintAssembly", "-XX:PrintAssemblyOptions=intel", 
     "-XX:+PrintSignatureHandlers"}) 
@Measurement(iterations = 5, time = 5, timeUnit = TimeUnit.SECONDS) 
@Warmup(iterations = 3, time = 2, timeUnit = TimeUnit.SECONDS) 
public class LinkedQueueBenchmark { 
private static final Unsafe unsafe = UnsafeProvider.getUnsafe(); 
private static final long offsetObject; 
private static final long offsetNext; 

private static final int THREADS = 5; 
private static class Node { 
    private volatile Node next; 
    public Node() {} 
} 

static { 
    try { 
     offsetObject = unsafe.objectFieldOffset(LinkedQueueBenchmark.class.getDeclaredField("object")); 
     offsetNext = unsafe.objectFieldOffset(Node.class.getDeclaredField("next")); 
    } catch (Exception ex) { throw new Error(ex); } 
} 

protected long t0,t1,t2,t3,t4,t5,t6,t7; 
private volatile Node object = new Node(null); 


@Threads(THREADS) 
@Benchmark 
public Node doTestCasSmart() { 
    Node current, o = new Node(); 
    for(;;) { 
     current = this.object; 
     if (unsafe.compareAndSwapObject(this, offsetObject, current, o)) { 
      //current.next = o; //Special line: 
      break; 
     } else { 
      LockSupport.parkNanos(1); 
     } 
    } 
    return current; 
} 
} 
  1. В текущем варианте у меня есть производительность ~ 55 OPS/нас
  2. Но, если я раскомментировать «Специальную линию» , или замените его на unsafe.putOrderedObject (в любом направлении - current.next = o или o.next = текущий), производительность ~ 2 ops/us.

Как я понимаю, это происходит с CPU-кэшами и, возможно, это очистка буфера хранилища. Если я заменил его на метод блокировки, без CAS, производительность будет 11-20 ops/us.
Я пытаюсь использовать LinuxPerfAsmProfiler и PrintAssembly, во втором случае я вижу:

....[Hottest Regions]............................................................................... 
25.92% 17.93% [0x7f1d5105fe60:0x7f1d5105fe69] in SpinPause (libjvm.so) 
17.53% 20.62% [0x7f1d5119dd88:0x7f1d5119de57] in ParMarkBitMap::live_words_in_range(HeapWord*, oopDesc*) const (libjvm.so) 
10.81% 6.30% [0x7f1d5129cff5:0x7f1d5129d0ed] in ParallelTaskTerminator::offer_termination(TerminatorTerminator*) (libjvm.so) 
    7.99% 9.86% [0x7f1d3c51d280:0x7f1d3c51d3a2] in com.jad.generated.LinkedQueueBenchmark_doTestCasSmart::doTestCasSmart_thrpt_jmhStub 

Может кто-нибудь объяснить мне, что на самом деле произошло? Почему это так медленно? Где здесь барьер для хранения? Почему putOrdered не работает? И как это исправить?

ответ

9

Правило: вместо поиска «расширенных» ответов вы должны сначала искать глупые ошибки.

SpinPause, ParMarkBitMap::live_words_in_range(HeapWord*, oopDesc*) и ParallelTaskTerminator::offer_termination(TerminatorTerminator*) - из нитей GC. Что вполне может означать, что большинство тестов производительности - GC. Действительно, запуск «специальная линия» раскомментируйте с -prof gc выходами:

# Run complete. Total time: 00:00:43 

Benchmark      Mode Cnt  Score Error Units 
LQB.doTestCasSmart   thrpt 5  5.930 ± 3.867 ops/us 
LQB.doTestCasSmart:·gc.time thrpt 5 29970.000    ms 

Таким образом, из 43 секунд для запуска, вы потратили 30 секунд делает GC. Или, даже на равнине -verbose:gc показывает это:

Iteration 3: [Full GC (Ergonomics) 408188K->1542K(454656K), 0.0043022 secs] 
[GC (Allocation Failure) 60422K->60174K(454656K), 0.2061024 secs] 
[GC (Allocation Failure) 119054K->118830K(454656K), 0.2314572 secs] 
[GC (Allocation Failure) 177710K->177430K(454656K), 0.2268396 secs] 
[GC (Allocation Failure) 236310K->236054K(454656K), 0.1718049 secs] 
[GC (Allocation Failure) 294934K->294566K(454656K), 0.2265855 secs] 
[Full GC (Ergonomics) 294566K->147408K(466432K), 0.7139546 secs] 
[GC (Allocation Failure) 206288K->205880K(466432K), 0.2065388 secs] 
[GC (Allocation Failure) 264760K->264312K(466432K), 0.2314117 secs] 
[GC (Allocation Failure) 323192K->323016K(466432K), 0.2183271 secs] 
[Full GC (Ergonomics) 323016K->322663K(466432K), 2.8058725 secs] 

2.8s полный ШС, что отстой. Примерно 5 секунд, проведенных в GC, на итерации, ограниченной 5 секундами. Это тоже отстой.

Почему? Ну, вы строите там связанный список. Разумеется, глава очереди недоступен, и все от головы до вашего object должно быть собрано. Но коллекция не мгновенная. Чем дольше очередь становится, тем больше памяти она потребляет, тем больше работы для GC, чтобы пересечь ее. Это положительный цикл обратной связи, который калечит выполнение. Поскольку элементы очереди все равно собираются, этот цикл обратной связи никогда не достигает OOME. Хранение intial object в новом поле head сделает тест, в конечном счете, НЕОЖИМ.

Таким образом, ваш вопрос не имеет никакого отношения ни к putOrdered, ни к барьерам памяти, ни к производительности в очереди, если быть откровенным. Я думаю, вам нужно пересмотреть то, что вы на самом деле проверяете. Проектирование теста таким образом, чтобы промежуточный участок памяти оставался неизменным на каждом вызове @Benchmark - это само по себе искусство.

+0

Спасибо, Алексей. –

+0

Я действительно не исследовал его на GC perf. Да, этот тест суррогатный. Я пытаюсь создать LIFO на основе CAS. У меня есть моя простая реализация, где метод * offer() * всегда получает текущий хвост (он всегда не равен нулю), CAS заменяет новый хвост и orderd put * next * в предыдущем хвосте. Профилер сказал, что этот метод является самым медленным. Я имею равную производительность с ConcurrentLinkedQueue, когда писатели <= читатели, и хуже в другом случае. Возможно, это тоже на GC. –

Смежные вопросы