observar la siguiente definición de una subclase de rosca (todo el archivo fuente de Java ejecutable se incluye al final de la pregunta para su conveniencia):asignación de matriz y el acceso a la máquina y la memoria virtual de contención de Java
final class Worker extends Thread {
Foo[] array = new Foo[1024];
int sz;
public Worker(int _sz) {
sz = _sz;
}
public void run() {
//Foo[] arr = new Foo[1024];
Foo[] arr = array;
loop(arr);
}
public void loop(Foo[] arr) {
int i = 0;
int pos = 512;
Foo v = new Foo();
while (i < sz) {
if (i % 2 == 0) {
arr[pos] = v;
pos += 1;
} else {
pos -= 1;
v = arr[pos];
}
i++;
}
}
}
Explicación: el programa comienza -Dpar
tales hilos, y establece el sz
de cada hilo a -Dsize/-Dpar
, donde -Dsize
y -Dpar
se establecen a través de la línea de comandos cuando se ejecuta el programa. Cada objeto de subproceso tiene un campo array
que se inicializa con una nueva matriz de elementos 1024
. El razonamiento es que queremos dividir una cantidad igual de trabajo entre un número diferente de subprocesos, esperamos que el programa se escale.
Cada subproceso se inicia y se mide el tiempo necesario para que se completen todos los subprocesos. Hacemos mediciones múltiples para contrarrestar cualquier efecto relacionado con JIT, como se muestra a continuación. Cada hilo hace un bucle. Dentro del ciclo, el hilo lee un elemento en la posición 512
en la matriz en iteraciones pares, y escribe el mismo elemento en 512
en iteraciones impares. Solo las variables locales se modifican de otra manera.
El programa completo está debajo.
Análisis:
probado con -verbose:gc
- no hay recolección de basura que se produce durante la ejecución de este programa.
comando Ejecutar:
java -Xmx512m -Xms512m -server -Dsize=500000000 -Dpar=1 org.scalapool.bench.MultiStackJavaExperiment 7
CASO 1: tiempos de funcionamiento de 1,2,4,8
hilos, en ese orden (7 repeticiones):
>>> All running times: [2149, 2227, 1974, 1948, 1803, 2283, 1878]
>>> All running times: [1140, 1124, 2022, 1141, 2028, 2004, 2136]
>>> All running times: [867, 1022, 1457, 1342, 1436, 966, 1531]
>>> All running times: [915, 864, 1245, 1243, 948, 790, 1007]
Mi pensamiento era que la escala no lineal se debe a la contención de memoria . Por cierto, las primeras iteraciones realmente funcionan mejor, esto podría deberse al hecho de que en diferentes iteraciones las matrices se asignan en diferentes áreas de memoria.
CASO 2: A continuación, I comentar la línea Foo[] arr = array
en el método de la rosca run
y asignar una nueva matriz en el método run
sí: Foo[] arr = new Foo[1024]
. Medidas:
>>> All running times: [2053, 1966, 2089, 1937, 2046, 1909, 2011]
>>> All running times: [1048, 1178, 1100, 1194, 1367, 1271, 1207]
>>> All running times: [578, 508, 589, 571, 617, 643, 645]
>>> All running times: [330, 299, 300, 322, 331, 324, 575]
Esta vez, todo se escala bastante como se esperaba. No me hubiera imaginado que la ubicación donde se asignó el conjunto juega ningún papel en absoluto, pero obviamente lo hace de alguna manera. Mi idea era que las matrices estaban previamente asignadas tan cerca unas de otras que algo de conflicto de memoria comenzó a suceder.
CASO 3: Para verificar esta hipótesis, no tengo sin comentar la línea Foo[] arr = array
de nuevo, pero esta vez inicializado el campo array
a new Foo[32000]
para asegurar que la ubicación en la memoria están escribiendo son suficientemente lejos unos de otros. Entonces, aquí estamos usando la matriz asignada durante la creación del objeto thread nuevamente, la diferencia con CASE1 es solo que la matriz es más grande.
>>> All running times: [2113, 1983, 2430, 2485, 2333, 2359, 2463]
>>> All running times: [1172, 1106, 1163, 1181, 1142, 1169, 1188]
>>> All running times: [578, 677, 614, 604, 583, 637, 597]
>>> All running times: [343, 327, 320, 330, 353, 320, 320]
Por lo tanto, parece que la causa de esto es la contención de la memoria.
La información de la plataforma:
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)
Pregunta: Esto es obviamente un problema de memoria en la contención. Pero ¿por qué sucede esto?
¿Se está realizando el análisis de escape? Si es así, ¿significa que toda la matriz se asigna en la pila cuando se creó en el método
run
en CASE2? ¿Cuáles son las condiciones exactas para esta optimización de tiempo de ejecución? Sin duda, la matriz no está asignada en la pila para 1 millón de elementos?Incluso si la matriz se asigna en la pila en lugar de ser asignado en el montón, matriz de dos accesos por diferentes hilos debe dividirse por lo menos 512 * 4bytes = 2 kb incluso en CASE1, donde las matrices son ! Eso es definitivamente más grande que cualquier línea de caché L1. Si estos efectos se deben a la compartición falsa, ¿cómo pueden las escrituras en varias líneas de caché totalmente independientes afectar el rendimiento tanto? (Una suposición aquí es que cada matriz ocupa un bloque contiguo de memoria en la JVM, que se asigna cuando se crea la matriz. No estoy seguro de que esto sea válido. Otra suposición es que las escrituras de la matriz no van hasta el final memoria, pero la caché L1 en cambio, como Intel Xeon tiene una arquitectura ccNUMA - corríjame si estoy equivocado)
Es posible que cada subproceso tenga su propia parte de montón local donde asigna nuevos objetos de forma independiente, y esto ¿Es la causa de menor contención cuando la matriz se asigna en el hilo? De ser así, ¿cómo se recoge esa área de basura acumulada si se comparten las referencias?
¿Por qué el aumento del tamaño de la matriz a ~ 32000 elementos mejoró la escalabilidad (disminución de la contención de la memoria)? ¿Qué es exactamente en la jerarquía de la memoria la causa de esto?
Por favor, sea preciso y respalde sus reclamos con referencias.
¡Gracias!
Todo el programa Java ejecutable:
import java.util.ArrayList;
class MultiStackJavaExperiment {
final class Foo {
int x = 0;
}
final class Worker extends Thread {
Foo[] array = new Foo[1024];
int sz;
public Worker(int _sz) {
sz = _sz;
}
public void run() {
Foo[] arr = new Foo[1024];
//Foo[] arr = array;
loop(arr);
}
public void loop(Foo[] arr) {
int i = 0;
int pos = 512;
Foo v = new Foo();
while (i < sz) {
if (i % 2 == 0) {
arr[pos] = v;
pos += 1;
} else {
pos -= 1;
v = arr[pos];
}
i++;
}
}
}
public static void main(String[] args) {
(new MultiStackJavaExperiment()).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 Worker(sz));
threads.get(i).start();
}
for (int i = 0; i < par; i++) {
try {
threads.get(i).join();
} catch (Exception e) {}
}
}
}
Es fácil perder el tiempo con los números y obtener los resultados que está buscando, gracias por echar un vistazo a mi respuesta. –
Gracias por la respuesta, pero ¿por qué la eliminaste? – axel22
No he leído su análisis y pregunta tan bien como debería y no creo que haya respondido correctamente a su pregunta –