Обратите внимание на следующую программу, написанную на Java (полная готовая версия приведена ниже, но важная часть программы приведена ниже):
import java.util.ArrayList;
/** A not easy to explain benchmark.
*/
class MultiVolatileJavaExperiment {
public static void main(String[] args) {
(new MultiVolatileJavaExperiment()).mainMethod(args);
}
int size = Integer.parseInt(System.getProperty("size"));
int par = Integer.parseInt(System.getProperty("par"));
public void mainMethod(String[] args) {
int times = 0;
if (args.length == 0) times = 1;
else times = Integer.parseInt(args[0]);
ArrayList < Long > measurements = new ArrayList < Long > ();
for (int i = 0; i < times; i++) {
long start = System.currentTimeMillis();
run();
long end = System.currentTimeMillis();
long time = (end - start);
System.out.println(i + ") Running time: " + time + " ms");
measurements.add(time);
}
System.out.println(">>>");
System.out.println(">>> All running times: " + measurements);
System.out.println(">>>");
}
public void run() {
int sz = size / par;
ArrayList < Thread > threads = new ArrayList < Thread > ();
for (int i = 0; i < par; i++) {
threads.add(new Reader(sz));
threads.get(i).start();
}
for (int i = 0; i < par; i++) {
try {
threads.get(i).join();
} catch (Exception e) {}
}
}
final class Foo {
int x = 0;
}
final class Reader extends Thread {
volatile Foo vfoo = new Foo();
Foo bar = null;
int sz;
public Reader(int _sz) {
sz = _sz;
}
public void run() {
int i = 0;
while (i < sz) {
vfoo.x = 1;
// with the following line commented
// the scalability is almost linear
bar = vfoo; // <- makes benchmark 2x slower for 2 processors - why?
i++;
}
}
}
}
Объяснение : Программа на самом деле очень проста.Он загружает целые числа size
и par
из системных свойств (переданных в jvm с флагом -D
) - это длина ввода и количество потоков, которые будут использоваться позже.Затем он анализирует первый аргумент командной строки, который говорит, сколько раз нужно повторить программу (мы хотим быть уверены, что JIT выполнил свою работу и провел более надежные измерения).
Вызван метод run
в каждом повторении.Этот метод просто запускает par
потоков, каждый из которых будет выполнять цикл с size / par
итерациями.Тело потока определено в классе Reader
.Каждое повторение цикла считывает энергозависимый элемент vfoo
и присваивает 1
его открытому полю.После этого vfoo
читается еще раз и присваивается энергонезависимому полю bar
.
Обратите внимание, как большую часть времени программа выполняет тело цикла, поэтомуrun
в теме - это фокус этого теста:
final class Reader extends Thread {
volatile Foo vfoo = new Foo();
Foo bar = null;
int sz;
public Reader(int _sz) {
sz = _sz;
}
public void run() {
int i = 0;
while (i < sz) {
vfoo.x = 1;
// with the following line commented
// the scalability is almost linear
bar = vfoo; // <- makes benchmark 2x slower for 2 processors - why?
i++;
}
}
}
Наблюдения : Запуск java -Xmx512m -Xms512m -server -Dsize=500000000 -Dpar=1 MultiVolatileJavaExperiment 10
на
Ubuntu Server 10.04.3 LTS
8 core Intel(R) Xeon(R) CPU X5355 @2.66GHz
~20GB ram
java version "1.6.0_26"
Java(TM) SE Runtime Environment (build 1.6.0_26-b03)
Java HotSpot(TM) 64-Bit Server VM (build 20.1-b02, mixed mode)
Я получаю следующее время:
>>> All running times: [821, 750, 1011, 750, 758, 755, 1219, 751, 751, 1012]
Теперь, установив -Dpar=2
, я получаю:
>>> All running times: [1618, 380, 1476, 1245, 1390, 1391, 1445, 1393, 1511, 1508]
По-видимому, по какой-то причине это не масштабируется - я бы ожидал, что второй результат будет в два разатак быстро (хотя, похоже, это происходит на одной из ранних итераций - 380ms
).
Интересно, что комментирование строки bar = vfoo
(которая даже не должна быть изменчивой записью),выдает следующие значения времени для -Dpar
, установленного на 1,2,4,8
.
>>> All running times: [762, 563, 563, 563, 563, 563, 570, 566, 563, 563]
>>> All running times: [387, 287, 285, 284, 283, 281, 282, 282, 281, 282]
>>> All running times: [204, 146, 143, 142, 141, 141, 141, 141, 141, 141]
>>> All running times: [120, 78, 74, 74, 81, 75, 73, 73, 72, 71]
Отлично масштабируется.
Анализ : Прежде всего, сборка мусора отсутствуетциклы происходят здесь (я также добавил -verbose:gc
, чтобы проверить это).
Я получаю аналогичные результаты на моем iMac.
EКаждый поток записывает в свое собственное поле, и разные экземпляры объекта Foo
, принадлежащие разным потокам, по-видимому, не попадают в одни и те же строки кэширования - добавление большего числа членов в Foo
для увеличения его размера не меняет измерения,Каждый экземпляр объекта потока имеет более чем достаточно полей для заполнения строки кэша L1.Так что это, вероятно, не проблема с памятью.
Моя следующая мысль была о том, что JIT
может делать что-то странное, потому что ранние итерации обычно do масштабируются, как и ожидалось в некомментированной версии, поэтому я проверил это, напечатав сборку (см. этот пост о том, как это сделать ).
java -Xmx512m -Xms512m -server -XX:CompileCommand=print,*Reader.run MultiVolatileJavaExperiment -Dsize=500000000 -Dpar=1 10
, и я получаю эти 2 вывода для 2 версий для метода Jotedrun
в Reader
.Версия с комментариями (правильно масштабируемая):
[Verified Entry Point]
0xf36c9fac: mov %eax,-0x3000(%esp)
0xf36c9fb3: push %ebp
0xf36c9fb4: sub $0x8,%esp
0xf36c9fba: mov 0x68(%ecx),%ebx
0xf36c9fbd: test %ebx,%ebx
0xf36c9fbf: jle 0xf36c9fec
0xf36c9fc1: xor %ebx,%ebx
0xf36c9fc3: nopw 0x0(%eax,%eax,1)
0xf36c9fcc: xchg %ax,%ax
0xf36c9fd0: mov 0x6c(%ecx),%ebp
0xf36c9fd3: test %ebp,%ebp
0xf36c9fd5: je 0xf36c9ff7
0xf36c9fd7: movl $0x1,0x8(%ebp)
---------------------------------------------
0xf36c9fde: mov 0x68(%ecx),%ebp
0xf36c9fe1: inc %ebx ; OopMap{ecx=Oop off=66}
;*goto
; - org.scalapool.bench.MultiVolatileJavaExperiment$Reader::run@21 (line 83)
---------------------------------------------
0xf36c9fe2: test %edi,0xf7725000 ; {poll}
0xf36c9fe8: cmp %ebp,%ebx
0xf36c9fea: jl 0xf36c9fd0
0xf36c9fec: add $0x8,%esp
0xf36c9fef: pop %ebp
0xf36c9ff0: test %eax,0xf7725000 ; {poll_return}
0xf36c9ff6: ret
0xf36c9ff7: mov $0xfffffff6,%ecx
0xf36c9ffc: xchg %ax,%ax
0xf36c9fff: call 0xf36a56a0 ; OopMap{off=100}
;*putfield x
; - org.scalapool.bench.MultiVolatileJavaExperiment$Reader::run@15 (line 79)
; {runtime_call}
0xf36ca004: call 0xf6f877a0 ; {runtime_call}
Версия без комментариев bar = vfoo
(немасштабируемая, более медленная):
[Verified Entry Point]
0xf3771aac: mov %eax,-0x3000(%esp)
0xf3771ab3: push %ebp
0xf3771ab4: sub $0x8,%esp
0xf3771aba: mov 0x68(%ecx),%ebx
0xf3771abd: test %ebx,%ebx
0xf3771abf: jle 0xf3771afe
0xf3771ac1: xor %ebx,%ebx
0xf3771ac3: nopw 0x0(%eax,%eax,1)
0xf3771acc: xchg %ax,%ax
0xf3771ad0: mov 0x6c(%ecx),%ebp
0xf3771ad3: test %ebp,%ebp
0xf3771ad5: je 0xf3771b09
0xf3771ad7: movl $0x1,0x8(%ebp)
-------------------------------------------------
0xf3771ade: mov 0x6c(%ecx),%ebp
0xf3771ae1: mov %ebp,0x70(%ecx)
0xf3771ae4: mov 0x68(%ecx),%edi
0xf3771ae7: inc %ebx
0xf3771ae8: mov %ecx,%eax
0xf3771aea: shr $0x9,%eax
0xf3771aed: movb $0x0,-0x3113c300(%eax) ; OopMap{ecx=Oop off=84}
;*goto
; - org.scalapool.bench.MultiVolatileJavaExperiment$Reader::run@29 (line 83)
-----------------------------------------------
0xf3771af4: test %edi,0xf77ce000 ; {poll}
0xf3771afa: cmp %edi,%ebx
0xf3771afc: jl 0xf3771ad0
0xf3771afe: add $0x8,%esp
0xf3771b01: pop %ebp
0xf3771b02: test %eax,0xf77ce000 ; {poll_return}
0xf3771b08: ret
0xf3771b09: mov $0xfffffff6,%ecx
0xf3771b0e: nop
0xf3771b0f: call 0xf374e6a0 ; OopMap{off=116}
;*putfield x
; - org.scalapool.bench.MultiVolatileJavaExperiment$Reader::run@15 (line 79)
; {runtime_call}
0xf3771b14: call 0xf70307a0 ; {runtime_call}
Различия в двух версиях находятся в пределах ---------
.Я ожидал найти инструкции по сборке в сборке, которые могли бы объяснить проблему с производительностью - хотя несколько дополнительных команд shift
, mov
и inc
могут повлиять на абсолютные значения производительности, я не понимаю, как они могут повлиять на масштабируемость.1083 *
Итак, я подозреваю, что это какая-то проблема с памятью, связанная с сохранением поля в классе.С другой стороны, я также склонен полагать, что JIT делает что-то смешное, потому что за одну итерацию измеренное время составляет в два раза быстрее, чем должно быть.
Может кто-нибудьобъясните, что здесь происходит?Пожалуйста, будьте точны и включайте ссылки, подтверждающие ваши требования.
Спасибо!
РЕДАКТИРОВАТЬ:
Вот байт-код для быстрой (масштабируемой) версии:
public void run();
LineNumberTable:
line 77: 0
line 78: 2
line 79: 10
line 83: 18
line 85: 24
Code:
Stack=2, Locals=2, Args_size=1
0: iconst_0
1: istore_1
2: iload_1
3: aload_0
4: getfield #7; //Field sz:I
7: if_icmpge 24
10: aload_0
11: getfield #5; //Field vfoo:Lorg/scalapool/bench/MultiVolatileJavaExperiment$Foo;
14: iconst_1
15: putfield #8; //Field org/scalapool/bench/MultiVolatileJavaExperiment$Foo.x:I
18: iinc 1, 1
21: goto 2
24: return
LineNumberTable:
line 77: 0
line 78: 2
line 79: 10
line 83: 18
line 85: 24
StackMapTable: number_of_entries = 2
frame_type = 252 /* append */
offset_delta = 2
locals = [ int ]
frame_type = 21 /* same */
Медленная (не масштабируемая) версия с bar = vfoo
:
public void run();
LineNumberTable:
line 77: 0
line 78: 2
line 79: 10
line 82: 18
line 83: 26
line 85: 32
Code:
Stack=2, Locals=2, Args_size=1
0: iconst_0
1: istore_1
2: iload_1
3: aload_0
4: getfield #7; //Field sz:I
7: if_icmpge 32
10: aload_0
11: getfield #5; //Field vfoo:Lorg/scalapool/bench/MultiVolatileJavaExperiment$Foo;
14: iconst_1
15: putfield #8; //Field org/scalapool/bench/MultiVolatileJavaExperiment$Foo.x:I
18: aload_0
19: aload_0
20: getfield #5; //Field vfoo:Lorg/scalapool/bench/MultiVolatileJavaExperiment$Foo;
23: putfield #6; //Field bar:Lorg/scalapool/bench/MultiVolatileJavaExperiment$Foo;
26: iinc 1, 1
29: goto 2
32: return
LineNumberTable:
line 77: 0
line 78: 2
line 79: 10
line 82: 18
line 83: 26
line 85: 32
StackMapTable: number_of_entries = 2
frame_type = 252 /* append */
offset_delta = 2
locals = [ int ]
frame_type = 29 /* same */
Чем больше я экспериментирую с этим, мне кажется, что это вообще не имеет никакого отношения к летучим веществам - это как-то связано с записью в объектные поля.Я догадываюсь, что это как-то связано с конфликтом памяти - что-то с кэшем и ложным разделением, хотя явной синхронизации вообще нет.
РЕДАКТИРОВАТЬ 2:
Интересно, что изменение программы происходит следующим образом:
final class Holder {
public Foo bar = null;
}
final class Reader extends Thread {
volatile Foo vfoo = new Foo();
Holder holder = null;
int sz;
public Reader(int _sz) {
sz = _sz;
}
public void run() {
int i = 0;
holder = new Holder();
while (i < sz) {
vfoo.x = 1;
holder.bar = vfoo;
i++;
}
}
}
решает проблему с масштабированием.Очевидно, объект Holder
, указанный выше, создается после запуска потока и, вероятно, размещается в другом сегменте памяти, который затем изменяется одновременно, в отличие от изменения поля bar
в объекте потока, которыйкак-то "закрывается" в памяти между различными экземплярами потока.