Micro-benchmark für parallel vs. sequentiell erzeugt unerwartetes Ergebnis

Antoras

Top Contributor
Hi,

ich bin mir nicht sicher ob ich einen Fehler im Programm habe oder ob ich bloß eine Optimierungsstrategie übersehe:

Java:
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;


public class ArraySummer {

	public static void main(final String[] args) {
		final int[] array = new int[200000000];

		final Random r = new Random();
		for (int i = 0; i < array.length; i++) {
			array[i] = Math.abs(r.nextInt() / 2);
		}

		for (int i = 0; i < 20; ++i) {
			parallSummer(array);
			sequentiellSummer(array);
		}

		System.out.println("start");
		for (int i = 0; i < 10; ++i) {
			testPar(array);
			testSeq(array);
			System.out.println();
		}
	}
	
	final static void testSeq(final int[] array) {
		final long s = System.nanoTime();
		sequentiellSummer(array);
		System.out.println("s:" + (System.nanoTime() - s) / 1e6 + "ms");
	}
	
	final static void testPar(final int[] array) {
		final long s = System.nanoTime();
		parallSummer(array);
		System.out.println("p:" + (System.nanoTime() - s) / 1e6 + "ms");
	}

	public static void sequentiellSummer(final int[] array) {
		new SeqSummer(array).calc();
	}

	public static void parallSummer(final int[] array) {
		final int processors = 2; // Runtime.getRuntime().availableProcessors();

		final List<Long> longs = new ArrayList<Long>();

		final Runnable merger = new Runnable() {

			@Override
			public void run() {
				calc();
			}

			long calc() {
				long sum = 0;
				for (final long i : longs) {
					sum += i;
				}
				return sum;
			}
		};

		final CyclicBarrier barrier = new CyclicBarrier(processors, merger);

		for (int part = 0; part < processors; part++) {
			new Thread(new AtomarSummer(barrier, array, processors, part, longs)).start();
		}
	}
}

class SeqSummer {

	private final int[] array;

	public SeqSummer(final int[] array) {
		this.array = array;
	}

	public long calc() {
		long sum = 0;
		for (int i = 0; i < array.length; ++i) {
			sum += array[i];
		}
		return sum;
	}
}

class AtomarSummer implements Runnable {

	private final CyclicBarrier barrier;
	private final int[] array;
	private final List<Long> longs;
	private final int start, end;

	public AtomarSummer(final CyclicBarrier barrier, final int[] array, final int maxPart,
			final int currentPart, final List<Long> longs) {
		this.barrier = barrier;
		this.array = array;
		this.longs = longs;

		start = (int) ((double) array.length / maxPart * currentPart);
		end = (int) ((double) array.length / maxPart * (currentPart + 1) - 1);
	}

	@Override
	public void run() {
		long sum = 0;

		for (int i = start; i < end; i++) {
			sum += array[i];
		}
		longs.add(sum);

		try {
			barrier.await();
		} catch (final InterruptedException e) {
			e.printStackTrace();
		} catch (final BrokenBarrierException e) {
			e.printStackTrace();
		}
	}
}

Output:

start
p:0.942543ms
s:378.924406ms

p:0.173262ms
s:322.728754ms

p:2.065446ms
s:320.918554ms

p:0.209188ms
s:292.466057ms

p:6.518078ms
s:343.991192ms

p:0.228163ms
s:332.049146ms

p:0.197921ms
s:330.990676ms

p:0.17808ms
s:325.774824ms

p:0.230276ms
s:325.778092ms

p:0.216491ms
s:362.563178ms

Das stimmt doch so nicht? Wiese benötigt der parallel ausgeführte Code praktisch keine Zeit?
 
Wenn ich das richtig sehe, flitzt der Main-Thread bei der parallelen Ausführung einfach durch und liefert daher diese Zeit.

Diese Änderung bringt realistische Ergebnisse (parallel etwa halb so lang wie seriell):
[JAVA=68]final CyclicBarrier barrier = new CyclicBarrier(processors + 1, merger); // + 1 für Main-Thread

for (int part = 0; part < processors; part++) {
new Thread(new AtomarSummer(barrier, array, processors, part, longs)).start(); // keine Änderung
}

try {
barrier.await(); // Main-Thread warten lassen
} catch (final InterruptedException e) {
e.printStackTrace();
} catch (final BrokenBarrierException e) {
e.printStackTrace();
}[/code]
Übrigens:
[JAVA=117]longs.add(sum);[/code]
Das ist nicht gerade thread-safe. 😉
 
An den Main-Thread hatte ich überhaupt nicht gedacht. Funktioniert prächtig nun, danke!

Nur der erwartetet Leistungsschub hab sich noch nicht erfüllt. Auf meiner 2-Kern CPU gibt es praktisch keinen Unterschied in der Laufzeit - erst mit 4 Kernen konnte ich die 50% Leistungsgewinn feststellen.

Das ist nicht gerade thread-safe.
Stimmt, ist mir auch schon aufgefallen. Darum werde ich mich als nächstes kümmern.
 
Mal getestet.

Intel(R) Core(TM)2 CPU 6320 @ 1.86GHz (2 Kerne):
Code:
p:312.727203ms
s:608.075777ms

p:309.608603ms
s:607.162109ms

p:310.647995ms
s:616.149362ms

p:310.580618ms
s:607.285416ms

p:312.999849ms
s:606.686449ms

p:317.859892ms
s:606.391413ms

p:312.53914ms
s:605.666813ms

p:312.262934ms
s:617.720498ms

p:311.720795ms
s:606.32457ms

p:309.291956ms
s:606.809437ms

Intel(R) Xeon(R) CPU X5670 @ 2.93GHz (4 Kerne):
Code:
p:132.549492ms
s:523.114756ms

p:153.491066ms
s:527.948164ms

p:132.757456ms
s:531.88554ms

p:140.850768ms
s:532.233419ms

p:135.681888ms
s:528.016683ms

p:170.084038ms
s:522.371119ms

p:132.910986ms
s:558.306694ms

p:132.090935ms
s:533.973896ms

p:132.870298ms
s:526.224823ms

p:139.975441ms
s:521.964137ms
 
@TO
Ganz erlich : lass die Finger von Micro-Benchmarks.

Wir alle wissen das Micro-Benchmarks weder aussagekräftig noch überhaupt sinnvoll sind (zumindest in fast allen Fällen). Daher sollte man hier eher weniger darüber diskutieren wie man diesen Benchmark nun richtig zum Laufen bekommt sondern eher darauf hinweisen das es so eigentlich recht wenig Sinn macht.

Nebenbei wurde mal irgendwo erwähnt das die VM in der Lage ist eine Leistungssteigerung zu erzielen so lange man CORE * 2 Threads verwendet. Also sind auf einem Dual-Core 4 Threads unter bestimmten Vorraussetzungen teilweise schneller als nur 2, 8 Threads hingegen wären aber wieder langsamer. Genau Erklärung findet man hier i-wo im Forum.
 
Micro-Benchmarks sind schwierig, können aber schon eine Tendenz liefern. Immer mit einer Prise Salz, aber wenn man ein paar Sachen beachtet (mehrere Durchläufe, Verfahren Abwechselnd mit steigender Problemgröße, genug Duchläufe damit die relevante Teile geJITtet werden, Dinge messen, die hinreichend kompliziert sind) kann man schon ganz passable Infos rausziehen. Mehr dazu steht in der Serie AngelikaLanger.com - Java Performance - Micro-Benchmarking - Angelika Langer Training/Consulting (mehrere, ausführliche Artikel - Links zu den anderen Teilen und weiterführende Links gibt's unten)
 
MicroBenchmark hat gesagt.:
Ganz erlich : lass die Finger von Micro-Benchmarks.
Wie soll ich herausfinden ob ein Algorithmus funktioniert bzw. besser ist als ein anderer wenn nicht durch Micro-Benchmarks? Dass die ungenau sind - vor allem innerhalb von VMs - weiß ich selber. Dennoch liefern sie Anhaltspunkte ob ich etwas richtig gemacht habe oder nicht.
 
Wie soll ich herausfinden ob ein Algorithmus funktioniert bzw. besser ist als ein anderer wenn nicht durch Micro-Benchmarks? Dass die ungenau sind - vor allem innerhalb von VMs - weiß ich selber. Dennoch liefern sie Anhaltspunkte ob ich etwas richtig gemacht habe oder nicht.

Wenn du wissen willst ob ein Algo performant ist oder nicht dann lass diesen ein paar Milliarden mal mit relativ viel Payload laufen das 1) JIT genug Zeit hat den Code soweit zu optimieren und 2) durch den längeren Zeitraum (du solltest auf 5min im "Idealfall" kommen) wird schneller ersichtlich wie performant etwas ist, denn der unterschied 100ms zu 150ms sagt noch nicht viel aus ... aber 3min vs 5min ist schon ein deutlicher anhaltspunkt. Wenn du also Benchmarken willst dann mit entsprechend viel Payload und möglichst vielen Durchläufen über eine möglichst lange Zeit. Micro-Benchmarking im ms oder ns bereich sind aussagelos.
 
Kann man denke ich so stehen lassen. Die Idee ist aber auf jeden Fall richtig. In einem größeren Programm ist es vor allem wichtig den Flaschenhals zu identifizieren. Es bringt nichts bei 90% des Codes paar ms rauszuschlagen in dem man irgendwelche Bitvergewaltigungen macht, wenn 10% des Code durch z.B. I/O Operationen die meiste Zeit kosten. Dann bei diesem langsamen Code ansetzen und dort versuchen z.B. durch Multithreading zu optimieren.
 

Zurück
Oben