How to Read One Billion Rows
Fast Enough Concurrently
🚥🏎️🏁🏆
René Schwietzke, Xceptance GmbH
1.0.2, 1996@reneschwietzke@foojay.social@reneschwietzke.bsky.social@ReneSchwietzke
#java #performance #tuning #qa #test #performancetest #loadtest
I care and share
c b
This presentation is licensed under
Creative Commons Attribution 4.0 International License
All source code is APL 2.0.
Third-party content could use a different license.
What is 1BRC and why the fuss?
A busy January 2024 for Java Devs
... write a Java program for retrieving temperature measurement values from a text file and calculate the min, mean, and max temperatures per weather station.
... one caveat, the file has 1,000,000,000 rows!
Zagreb;19.7
Havana;36.2
Mexicali;27.3
Marseille;15.4
Banjul;24.2
Canberra;0.8
Mandalay;19.3
Bridgetown;29.6
Chihuahua;10.9
Honiara;35.3
Gangtok;-1.7
Houston;32.4
Zürich;13.8
Kolkata;20.6
Tripoli;8.9
Karachi;28.3
Zagreb;13.2
Palm Springs;20.3
Lhasa;6.2
Sacramento;28.3
Edmonton;5.6
Toluca;14.0
El Paso;24.0
Antsiranana;24.9
Tromsø;-3.9
# This is TreeMap::toString
# Data shown here is formatted for screen
{
Abha=5.0/18.0/27.4, Abidjan=15.7/26.0/34.1,
Abéché=12.1/29.4/35.6, Accra=14.7/26.4/33.1,
Addis Ababa=2.1/16.0/24.3, Adelaide=4.1/17.3/29.7,
...
Willemstad=-7.9,28.2,57.7, Winnipeg=-28.1,3.0,39.4,
Wrocław=-27.5,9.6,50.2, Xi'an=-22.5,14.0,49.1,
Yakutsk=-42.2,-8.8,26.4, Yangon=-2.6,27.6,59.0,
Yaoundé=-10.1,23.6,58.3, Yellowknife=-38.4,-4.0,40.0,
Yerevan=-18.4,12.4,49.6, Yinchuan=-25.4,9.5,42.0,
Zagreb=-25.3,10.6,48.4, Zanzibar City=-5.0,26.1,61.2,
Zürich=-20.9,9.4,43.5, Ürümqi=-26.8,7.4,43.7,
İzmir=-15.8,18.0,50.1
}
Fewer Questions, More Doing
\nWhat do you think? Guess some numbers.
| Test | Cores | Time (s) | % |
|---|---|---|---|
| Baseline | 1 | 125.1 s | 100.0% |
| Thomas Wuerthinger | 1 | 7.4 s | 5.9% |
| Previous 20% Goal / Achieved | 1 | 25.0 s / 19.5 s | 20.0% / 15.6% |
| Baseline | 8 | 77.3 s | 61.8% |
| Thomas Wuerthinger | 8 | 1.6 s | 1.2% |
| Thomas Wuerthinger | 16 | 0.85 s | 0.7% |
| Today's 20% Goal | 8 | 15.5 s | 20.0% |
Hetzner, Finland, Dedicated Machine, AMD, 16 core, 64 GB, April 2026;
taskset -c 0-7 java...-Xms2g -Xmx2g; Thomas Wuerthinger: GraalVM 21 Native Image
What We Will Do and What We Won't
✅🫷🚫
Just to Make Things Easier
Berlin=23615/-0.412/6.310/15.243Goal: All code is still easily understandable.
The Original Code
private static record Measurement(String station, double value) {
private Measurement(String[] parts) {
this(parts[0], Double.parseDouble(parts[1]));
}
}
Collector<Measurement, MeasurementAggregator, ResultRow> collector = Collector.of(
MeasurementAggregator::new,
(a, m) -> {
a.min = Math.min(a.min, m.value);
a.max = Math.max(a.max, m.value);
a.sum += m.value;
a.count++;
},
(agg1, agg2) -> {
var res = new MeasurementAggregator();
res.min = Math.min(agg1.min, agg2.min);
res.max = Math.max(agg1.max, agg2.max);
res.sum = agg1.sum + agg2.sum;
res.count = agg1.count + agg2.count;
return res;
},
agg -> {
return new ResultRow(agg.count, agg.sum, agg.min, agg.max);
});
Map<String, ResultRow> measurements =
Files.lines(Paths.get(fileName))
.map(l -> l.split(";"))
.map(a -> new Measurement(a))
.collect(Collectors.groupingBy(m -> m.station(), collector));
return new TreeMap<>(measurements).toString();
org.rschwietzke.parallel.BRC001_Baseline
github.com/rschwietzke/1brc-the-first-80-meters.git
The basic concept of the 1BRC pipeline
| Test | C | Time (s) | % |
|---|---|---|---|
| BRC001_Baseline | 1 | 125.1 | 100.0 |
| BRC004_...w/o Collector | 1 | 105.6 | 84.4 |
| BRC003_BaselineFileAndSplit | 1 | 82.5 | 65.9 |
| BRC002_BaselineFileOnly | 1 | 44.7 | 35.7 |
Numbers between slides might vary.
github.com/rschwietzke/1brc-the-first-80-meters.git
Just Trying To Use What We Get For Free
Where it can be parallelized
.parallel()Collectors.groupingByConcurrent
Map<String, ResultRow> measurements =
Files.lines(Paths.get(fileName)).parallel()
.map(l -> l.split(";"))
.map(a -> new Measurement(a))
.collect(
Collectors.groupingBy(m -> m.station(), collector));
Map<String, ResultRow> measurements =
Files.lines(Paths.get(fileName))
.map(l -> l.split(";"))
.map(a -> new Measurement(a)).parallel()
.collect(
Collectors.groupingBy(m -> m.station(), collector));
Map<String, ResultRow> measurements =
Files.lines(Paths.get(fileName)).parallel()
.map(l -> l.split(";")).parallel()
.map(a -> new Measurement(a)).parallel()
.collect(
Collectors.groupingBy(m -> m.station(), collector));
...
.collect(
Collectors.groupingByConcurrent(m -> m.station(), collector));
Map<String, ResultRow> measurements =
Files.lines(Paths.get(fileName)).parallel()
.map(l -> new Measurement(l.split(";")))
.collect(
Collectors.groupingByConcurrent(m -> m.station(), collector));
org.rschwietzke.parallel.BRC010* to BRC016*
| Test | C | Time | % |
|---|---|---|---|
| BRC001_Baseline | 1 | 125.1 | 100.0 |
| BRC002_BaselineFileOnly | 1 | 44.7 | 35.7 |
| BRC003_BaselineFileAndSplit | 1 | 82.5 | 65.9 |
| BRC004_...w/o Collector | 1 | 105.6 | 84.4 |
| BRC010_ParallelAfterRead | 8 | 77.3 | 61.8 |
| BRC011_ParallelAfterMapping | 8 | 77.2 | 61.7 |
| BRC012_ParallelBoth | 8 | 77.3 | 61.8 |
| BRC013_ParallelAll | 8 | 77.2 | 61.7 |
| BRC014_ParallelGrouping | 8 | 79.1 | 63.2 |
| BRC015_Parallel_NoCollector | 8 | 74.5 | 59.6 |
| BRC016_Compact | 8 | 79.0 | 63.1 |
// BRC014_ParallelGrouping
Map<String, ResultRow> measurements =
Files.lines(Paths.get(fileName)).parallel()
.map(l -> l.split(";")).parallel()
.map(a -> new Measurement(a))
.collect(
Collectors.groupingByConcurrent(
m -> m.station(), collector));
// BRC016_Compact
Map<String, ResultRow> measurements =
Files.lines(Paths.get(fileName)).parallel()
.map(l -> new Measurement(l.split(";")))
.collect(
Collectors.groupingByConcurrent(
m -> m.station(), collector));
parallel()more than once is not doing anything. It is not working as one might imagine.
Streams are Hard to Tune
📜
@Override
public String run(final String fileName) throws IOException {
// our storage
final Map<String, Temperatures> cities = new HashMap<>();
// open the file
try (var reader = Files.newBufferedReader(Paths.get(fileName))) {
String line;
// read all lines until end of file
while ((line = reader.readLine()) != null) {
// split the line
final String[] cols = line.split(";");
// first is the city
final String city = cols[0];
// second our double temperature
final double temperature = Double.parseDouble(cols[1]);
// get us our measurement record
var measurement = new Temperatures(temperature);
// store it, when it exists, merge both measurements otherwise
cities.merge(city, measurement, (t1, t2) -> t1.merge(t2));
}
}
// ok, we got everything, now we need to order it
return new TreeMap<String, Temperatures>(cities).toString();
}
org.rschwietzke.parallel.BRC020_NoStream
| Test | C | Time | % |
|---|---|---|---|
| BRC001_Baseline | 1 | 125.1 | 100.0 |
| BRC002_BaselineFileOnly | 1 | 44.7 | 35.7 |
| BRC003_BaselineFileAndSplit | 1 | 82.5 | 65.9 |
| BRC004_...w/o Collector | 1 | 105.6 | 84.4 |
| BRC010_ParallelAfterRead | 8 | 77.3 | 61.8 |
| BRC011_ParallelAfterMapping | 8 | 77.2 | 61.7 |
| BRC012_ParallelBoth | 8 | 77.3 | 61.8 |
| BRC013_ParallelAll | 8 | 77.2 | 61.7 |
| BRC014_ParallelGrouping | 8 | 79.1 | 63.2 |
| BRC015_Parallel_NoCollector | 8 | 74.5 | 59.6 |
| BRC016_Compact | 8 | 79.0 | 63.1 |
| BRC020_NoStream | 1 | 127.6 | 102.1 |
| BRC021_NoStreamFileOnly | 1 | 41.7 | 33.3 |
The updated flow w/o streams
Brute Force Beginner Parallelism
🧵🔀
Producer-Consumer
FileReaderMap::mergeBrute force beginner parallelism
public String run(final String fileName)
{
// let's do a raw and basic old-school implementation without
// most modern Java help
BlockingQueue<String> splittingQueueInput = new LinkedBlockingDeque<>(100);
BlockingQueue<String[]> measureQueueInput = new LinkedBlockingDeque<>(100);
BlockingQueue<Temperatures> mappingQueueInput = new LinkedBlockingDeque<>(100);
int consumerCount = Math.max(1, (this.getThreadCount() - 2) / 2);
var readerThread = new ReaderThread(Paths.get(fileName), splittingQueueInput, consumerCount);
readerThread.start();
for (int i = 0; i < consumerCount; i++)
{
var sThread = new SplittingThread(splittingQueueInput, measureQueueInput);
sThread.start();
var mThread = new MeasurementThread(measureQueueInput, mappingQueueInput);
mThread.start();
}
var mapperThread = new MapperThread(mappingQueueInput, consumerCount);
mapperThread.start();
// wait till done
try
{
mapperThread.join();
}
catch (InterruptedException e)
{
throw new RuntimeException(e);
}
// get our result
var cities = mapperThread.getCities();
// ok, we got everything, now we need to order it
return new TreeMap<String, Temperatures>(cities).toString();
}
org.rschwietzke.parallel.BRC025_1_N_N_1
Let's try some variations on the theme
BRC025_1_N_N_1: 1 producer, N splitters, N measurement, 1 aggregatorBRC026_1_N_N_1_LargeBuffers: BRC025_1_N_N_1 with buffers of 100,000 instead of 100BRC027_1_N_N_1_Batches: BRC026_1_N_N_1_LargeBuffers but batches of 5,000 entriesBRC028_1_N_1: 1 producer, N split + measurement, 1 aggregator| Test | C | Time | % |
|---|---|---|---|
| BRC001_Baseline | 1 | 125.1 | 100.0 |
| BRC002_BaselineFileOnly | 1 | 44.7 | 35.7 |
| BRC003_BaselineFileAndSplit | 1 | 82.5 | 65.9 |
| BRC004_...w/o Collector | 1 | 105.6 | 84.4 |
| BRC010_ParallelAfterRead | 8 | 77.3 | 61.8 |
| BRC011_ParallelAfterMapping | 8 | 77.2 | 61.7 |
| BRC012_ParallelBoth | 8 | 77.3 | 61.8 |
| BRC013_ParallelAll | 8 | 77.2 | 61.7 |
| BRC014_ParallelGrouping | 8 | 79.1 | 63.2 |
| BRC015_Parallel_NoCollector | 8 | 74.5 | 59.6 |
| BRC016_Compact | 8 | 79.0 | 63.1 |
| BRC020_NoStream | 1 | 127.6 | 102.1 |
| BRC021_NoStreamFileOnly | 1 | 41.7 | 33.3 |
| BRC025_1_N_N_1 | 8 | 659.0 | 530.2 |
| BRC026_1_N_N_1_LargeBuffers | 8 | 498.6 | 401.1 |
| BRC027_1_N_N_1_Batches | 8 | 51.9 | 41.8 |
| BRC028_1_N_1 | 8 | ERROR | - |
Reduce the Data That Needs to Be Handed Over!
🗜
Large Tasks for the Consumers
One large operation per worker, not a chain of small ones
Let's try some variations on the theme
BRC030_1_N: 1:N, main thread reduces the intermediate resultsBRC032_1_N_Batched: BRC030_1_N with batches of 1,000 entriesBRC034_1_N_Virtual: BRC032_1_N_Batched, all virtual threads (manually created)BRC036_1_N_ExecutorPool: BRC032_1_N_Batched with Thread ExecutorPoolBRC038_1_N_VThreadsAreTasks: BRC032_1_N_Batched with batches as tasks into virtual thread executor poolLooks Different
| Test | C | Time | % |
|---|---|---|---|
| BRC001_Baseline | 1 | 125.1 | 100.0 |
| BRC002_BaselineFileOnly | 1 | 44.7 | 35.7 |
| BRC003_BaselineFileAndSplit | 1 | 82.5 | 65.9 |
| BRC004_...w/o Collector | 1 | 105.6 | 84.4 |
| BRC010_ParallelAfterRead | 8 | 77.3 | 61.8 |
| BRC011_ParallelAfterMapping | 8 | 77.2 | 61.7 |
| BRC012_ParallelBoth | 8 | 77.3 | 61.8 |
| BRC013_ParallelAll | 8 | 77.2 | 61.7 |
| BRC014_ParallelGrouping | 8 | 79.1 | 63.2 |
| BRC015_Parallel_NoCollector | 8 | 74.5 | 59.6 |
| BRC016_Compact | 8 | 79.0 | 63.1 |
| BRC020_NoStream | 1 | 127.6 | 102.1 |
| BRC021_NoStreamFileOnly | 1 | 41.7 | 33.3 |
| BRC025_1_N_N_1 | 8 | 659.0 | 530.2 |
| BRC026_1_N_N_1_LargeBuffers | 8 | 498.6 | 401.1 |
| BRC027_1_N_N_1_Batches | 8 | 51.9 | 41.8 |
| BRC028_1_N_1 | 8 | ERROR | - |
| Test | C | Time | % |
|---|---|---|---|
| BRC030_1_N | 8 | 483.5 | 389.0 |
| BRC032_1_N_Batched | 8 | 102.3 | 82.3 |
| BRC034_1_N_Virtual | 8 | 104.6 | 84.2 |
| BRC036_1_N_ExecutorPool | 8 | 48.5 | 39.0 |
| BRC038_1_N_VThreadsAreTasks | 8 | 47.0 | 37.8 |
We Cannot Move Faster Than
the Slowest Piece of Work
🧗♂️
We Cannot Split The Reader Easily
BufferedReader reads charchar has unknown byte backingcharRead data carefully
| Test | C | Time | % |
|---|---|---|---|
| BRC001_Baseline | 1 | 125.1 | 100.0 |
| BRC002_BaselineFileOnly | 1 | 44.7 | 35.7 |
| BRC003_BaselineFileAndSplit | 1 | 82.5 | 65.9 |
| BRC004_...w/o Collector | 1 | 105.6 | 84.4 |
| BRC010_ParallelAfterRead | 8 | 77.3 | 61.8 |
| BRC011_ParallelAfterMapping | 8 | 77.2 | 61.7 |
| BRC012_ParallelBoth | 8 | 77.3 | 61.8 |
| BRC013_ParallelAll | 8 | 77.2 | 61.7 |
| BRC014_ParallelGrouping | 8 | 79.1 | 63.2 |
| BRC015_Parallel_NoCollector | 8 | 74.5 | 59.6 |
| BRC016_Compact | 8 | 79.0 | 63.1 |
| BRC020_NoStream | 1 | 127.6 | 102.1 |
| BRC021_NoStreamFileOnly | 1 | 41.7 | 33.3 |
| BRC025_1_N_N_1 | 8 | 659.0 | 530.2 |
| BRC026_1_N_N_1_LargeBuffers | 8 | 498.6 | 401.1 |
| BRC027_1_N_N_1_Batches | 8 | 51.9 | 41.8 |
| BRC028_1_N_1 | 8 | ERROR | - |
| Test | C | Time | % |
|---|---|---|---|
| BRC032_1_N_Batched | 8 | 102.3 | 82.3 |
| BRC034_1_N_Virtual | 8 | 104.6 | 84.2 |
| BRC036_1_N_ExecutorPool | 8 | 48.5 | 39.0 |
| BRC038_1_N_VThreadsAreTasks | 8 | 47.0 | 37.8 |
| BRC040_N_ConcurrentRead | 8 | 6.4 | 5.1 |
| BRC040_N_ConcurrentRead | 16 | 3.7 | 3.0 |
They Said, Use That!
Using what Java recommends
Divide, Wait, Continue
How to play it
| Test | C | Time | % |
|---|---|---|---|
| BRC001_Baseline | 1 | 125.1 | 100.0 |
| BRC002_BaselineFileOnly | 1 | 44.7 | 35.7 |
| BRC003_BaselineFileAndSplit | 1 | 82.5 | 65.9 |
| BRC004_...w/o Collector | 1 | 105.6 | 84.4 |
| BRC010_ParallelAfterRead | 8 | 77.3 | 61.8 |
| BRC011_ParallelAfterMapping | 8 | 77.2 | 61.7 |
| BRC012_ParallelBoth | 8 | 77.3 | 61.8 |
| BRC013_ParallelAll | 8 | 77.2 | 61.7 |
| BRC014_ParallelGrouping | 8 | 79.1 | 63.2 |
| BRC015_Parallel_NoCollector | 8 | 74.5 | 59.6 |
| BRC016_Compact | 8 | 79.0 | 63.1 |
| BRC020_NoStream | 1 | 127.6 | 102.1 |
| BRC021_NoStreamFileOnly | 1 | 41.7 | 33.3 |
| BRC025_1_N_N_1 | 8 | 659.0 | 530.2 |
| BRC026_1_N_N_1_LargeBuffers | 8 | 498.6 | 401.1 |
| BRC027_1_N_N_1_Batches | 8 | 51.9 | 41.8 |
| BRC028_1_N_1 | 8 | ERROR | - |
| Test | C | Time | % |
|---|---|---|---|
| BRC032_1_N_Batched | 8 | 102.3 | 82.3 |
| BRC034_1_N_Virtual | 8 | 104.6 | 84.2 |
| BRC036_1_N_ExecutorPool | 8 | 48.5 | 39.0 |
| BRC038_1_N_VThreadsAreTasks | 8 | 47.0 | 37.8 |
| BRC040_N_ConcurrentRead | 8 | 6.4 | 5.1 |
| BRC042_ForkJoinPool | 8 | 26.4 | 21.1 |
| BRC043_..._NoReducer | 8 | 26.0 | 20.8 |
| BRC044_..._SelfSplitting | 8 | 25.3 | 20.2 |
| BRC047_VirtualThreads | 8 | 42.8 | 34.2 |
| BRC048_VT_ChunkSize_10MB | 8 | 25.1 | 20.1 |
Has the Lemon Been Squeezed Enough?
🍋🟩 🍋🟩 🍋🟩 🍋🟩 🍋🟩
Performance counter stats for 'java -Xms2g -Xmx2g -cp target/classes/ org.rschwietzke.parallel.BRC001_Baseline -f data-1000m.txt -wc 0 -mc 1 -t 8':
130,376.50 msec task-clock # 1.020 CPUs utilized
18,943 context-switches # 145.295 /sec
4,788 cpu-migrations # 36.724 /sec
326,998 page-faults # 2.508 K/sec
👉 464,798,376,203 cycles # 3.565 GHz
54,941,360,494 stalled-cycles-frontend # 11.82% frontend cycles idle
👉2,380,905,128,702 instructions # 5.12 insn per cycle
# 0.02 stalled cycles per insn
549,265,356,816 branches # 4.213 G/sec
2,342,277,605 branch-misses # 0.43% of all branches
⏱127.773261850 seconds time elapsed
124.145161000 seconds user
5.642008000 seconds sys
Performance counter stats for 'java -Xms2g -Xmx2g -cp target/classes/ org.rschwietzke.parallel.BRC044_ForkJoinPool_SelfSplitting -f data-1000m.txt -wc 0 -mc 1 -t 8':
195,689.07 msec task-clock # 7.527 CPUs utilized
26,837 context-switches # 137.141 /sec
4,385 cpu-migrations # 22.408 /sec
333,597 page-faults # 1.705 K/sec
👉 694,817,257,247 cycles # 3.551 GHz
124,646,585,103 stalled-cycles-frontend # 17.94% frontend cycles idle
👉2,160,182,371,314 instructions # 3.11 insn per cycle
# 0.06 stalled cycles per insn
495,875,578,633 branches # 2.534 G/sec
2,251,135,482 branch-misses # 0.45% of all branches
⏱ 25.997951402 seconds time elapsed
189.993214000 seconds user
4.836329000 seconds sys
org.rschwietzke.parallel.BRC0
What Would Be Theoretically Possible?
IPS: Instructions per Second, IPC: Instructions per Cycle
Asking a Profiler
🔳🕒🕵️
Checking CPU time of BRC048_VT_ChunkSize_10MB
Checking allocations of BRC048_VT_ChunkSize_10MB
String::split does a lot of work
line.split() is slowString[]
while ((line = r.readln()) != null) {
var splitData = line.split(";");
// first is the city
var city = splitData[0];
// second our double temperature
var temperature = Double.parseDouble(splitData[1]);
// get us our measurement record
var measurement = new Temperatures(city, temperature);
cities.merge(city, measurement, (t1, t2) -> t1.merge(t2));
}
while ((line = r.readln()) != null) {
// split the line using indexOf
final int semicolon = line.indexOf(';');
final String city = line.substring(0, semicolon);
final String temperatureString = line.substring(semicolon + 1);
// second our double temperature
var temperature = Double.parseDouble(temperatureString);
// get us our measurement record
var measurement = new Temperatures(city, temperature);
cities.merge(city, measurement, (t1, t2) -> t1.merge(t2));
}
| Test | C | Time | % | Cycles (B) | Instr (B) | Ctx Switches | CPU Migr | IPC |
|---|---|---|---|---|---|---|---|---|
| BRC001_Baseline | 1 | 124.5 | 100.0 | 459.5 | 2,351.5 | 18,946 | 3,870 | 5.12 |
| BRC010_ParallelAfterRead | 8 | 77.3 | 62.1 | 1,271.4 | 3,911.2 | 68,644 | 13,748 | 3.08 |
| BRC044..._SelfSplitting | 8 | 25.4 | 20.4 | 715.9 | 2,171.0 | 27,759 | 4,405 | 3.03 |
| BRC048_VT_ChunkSize_10MB | 8 | 25.4 | 20.4 | 738.6 | 2,188.7 | 27,899 | 4,467 | 2.96 |
| BRC060_44_SplitTune | 8 | 19.7 | 15.8 | 569.7 | 1,598.7 | 22,025 | 3,597 | 2.81 |
Double::parseDoubleString to double Parsing Is Heavy Too
Double::parseDouble is heavy
private static final int DIGITOFFSET = 48;
private static final double[] multipliers = {
1, 1, 0.1, 0.01, 0.001, 0.000_1, 0.000_01, 0.000_001, 0.000_000_1, 0.000_000_01,
0.000_000_001, 0.000_000_000_1, 0.000_000_000_01, 0.000_000_000_001, 0.000_000_000_000_1,
0.000_000_000_000_01, 0.000_000_000_000_001, 0.000_000_000_000_000_1, 0.000_000_000_000_000_01};
public static double parseDouble(final String s)
{
final int negative = s.charAt(0) == '-' ? 1 : 0;
long value = 0;
final int end = s.length();
int decimalPos = end;
for (int i = negative; i < end; i++)
{
final int d = s.charAt(i);
if (d == '.')
{
decimalPos = i;
continue;
}
final int v = d - DIGITOFFSET;
value = value * 10 + v;
}
// set the sign
value = negative == 1 ? -value : value;
// adjust the decimal places
return value * multipliers[end - decimalPos];
}
| Test | C | Time | % | Cycles (B) | Instr (B) | Ctx Switches | CPU Migr | IPC |
|---|---|---|---|---|---|---|---|---|
| BRC001_Baseline | 1 | 124.5 | 100.0 | 459.5 | 2,351.5 | 18,946 | 3,870 | 5.12 |
| BRC010_ParallelAfterRead | 8 | 77.3 | 62.1 | 1,271.4 | 3,911.2 | 68,644 | 13,748 | 3.08 |
| BRC044..._SelfSplitting | 8 | 25.4 | 20.4 | 715.9 | 2,171.0 | 27,759 | 4,405 | 3.03 |
| BRC048_VT_ChunkSize_10MB | 8 | 25.4 | 20.4 | 738.6 | 2,188.7 | 27,899 | 4,467 | 2.96 |
| BRC060_44_SplitTune | 8 | 19.7 | 15.8 | 569.7 | 1,598.7 | 22,025 | 3,597 | 2.81 |
| BRC062_ParseDouble | 8 | 17.6 | 14.1 | 489.1 | 1,352.3 | 18,593 | 3,027 | 2.76 |
We Don't Need a Fresh String for Parsing
String creation
while ((line = r.readln()) != null)
{
// split the line using indexOf
final int semicolon = line.indexOf(';');
final String city = line.substring(0, semicolon);
final String temperatureString = line.substring(semicolon + 1);
// second our double temperature
var temperature = parseDouble(temperatureString);
// get us our measurement record
var measurement = new Temperatures(city, temperature);
cities.merge(city, measurement, (t1, t2) -> t1.merge(t2));
}
while ((line = r.readln()) != null)
{
// split the line using indexOf
final int semicolon = line.indexOf(';');
final String city = line.substring(0, semicolon);
// second our double temperature
final double temperature = parseDouble(line,
semicolon + 1, line.length() - 1);
// get us our measurement record
var measurement = new Temperatures(city, temperature);
cities.merge(city, measurement, (t1, t2) -> t1.merge(t2));
}
| Test | C | Time | % | Cycles (B) | Instr (B) | Ctx Switches | CPU Migr | IPC |
|---|---|---|---|---|---|---|---|---|
| BRC001_Baseline | 1 | 124.5 | 100.0 | 459.5 | 2,351.5 | 18,946 | 3,870 | 5.12 |
| BRC010_ParallelAfterRead | 8 | 77.3 | 62.1 | 1,271.4 | 3,911.2 | 68,644 | 13,748 | 3.08 |
| BRC044..._SelfSplitting | 8 | 25.4 | 20.4 | 715.9 | 2,171.0 | 27,759 | 4,405 | 3.03 |
| BRC048_VT_ChunkSize_10MB | 8 | 25.4 | 20.4 | 738.6 | 2,188.7 | 27,899 | 4,467 | 2.96 |
| BRC060_44_SplitTune | 8 | 19.7 | 15.8 | 569.7 | 1,598.7 | 22,025 | 3,597 | 2.81 |
| BRC062_ParseDouble | 8 | 17.6 | 14.1 | 489.1 | 1,352.3 | 18,593 | 3,027 | 2.76 |
| BRC064_NoExtraString | 8 | 16.3 | 13.1 | 446.6 | 1,208.3 | 15,823 | 2,542 | 2.71 |
Who Needs double Anyway?
double uses more memory and is slower to processdouble handling till we really need a double result."
private static final int DIGITOFFSET = 48;
/**
* Parses a double but ends up with an int, only because we know
* the format of the results -99.9 to 99.9
*/
public static int parseInteger(final String s, final int offset, final int end)
{
final int negative = s.charAt(offset) == '-' ? offset + 1 : offset;
int value = 0;
for (int i = negative; i < end; i++)
{
final int d = s.charAt(i);
if (d == '.')
{
continue;
}
final int v = d - DIGITOFFSET;
value = value * 10 + v;
}
value = negative != offset ? -value : value;
return value;
}
| Test | C | Time | % | Cycles (B) | Instr (B) | Ctx Switches | CPU Migr | IPC |
|---|---|---|---|---|---|---|---|---|
| BRC001_Baseline | 1 | 124.5 | 100.0 | 459.5 | 2,351.5 | 18,946 | 3,870 | 5.12 |
| BRC010_ParallelAfterRead | 8 | 77.3 | 62.1 | 1,271.4 | 3,911.2 | 68,644 | 13,748 | 3.08 |
| BRC044..._SelfSplitting | 8 | 25.4 | 20.4 | 715.9 | 2,171.0 | 27,759 | 4,405 | 3.03 |
| BRC048_VT_ChunkSize_10MB | 8 | 25.4 | 20.4 | 738.6 | 2,188.7 | 27,899 | 4,467 | 2.96 |
| BRC060_44_SplitTune | 8 | 19.7 | 15.8 | 569.7 | 1,598.7 | 22,025 | 3,597 | 2.81 |
| BRC062_ParseDouble | 8 | 17.6 | 14.1 | 489.1 | 1,352.3 | 18,593 | 3,027 | 2.76 |
| BRC064_NoExtraString | 8 | 16.3 | 13.1 | 446.6 | 1,208.3 | 15,823 | 2,542 | 2.71 |
| BRC066_Integer | 8 | 14.6 | 11.7 | 408.6 | 1,110.1 | 14,682 | 2,512 | 2.72 |
Loops Incur Penalties
private static final int DIGITOFFSET = 48;
/**
* Parses a double but ends up with an int, only because we know
* the format of the results -99.9 to 99.9
*
* Best case, two branches
* Worst case, two branches
*/
public static int parseInteger(final String s, int offset, final int end)
{
int l = end - offset;
var p0 = s.charAt(end - 1);
var p2 = s.charAt(end - 3) * 10;
var value = p2 + p0 - (DIGITOFFSET * 10 + DIGITOFFSET);
final char firstChar = s.charAt(offset);
if (firstChar == '-') {
if (l == 5) {
// -99.9
var p3 = s.charAt(end - 4) * 100;
value = p3 + value - (DIGITOFFSET * 100);
}
else {
// -9.9
}
return -value;
}
else {
if (l == 4) {
// 99.9
// we can use firstChar directly as we know its not '-'
var p3 = firstChar * 100;
value = p3 + value - (DIGITOFFSET * 100);
}
else {
// 9.9
}
return value;
}
}
| Test | C | Time | % | Cycles (B) | Instr (B) | Ctx Switches | CPU Migr | IPC |
|---|---|---|---|---|---|---|---|---|
| BRC001_Baseline | 1 | 124.5 | 100.0 | 459.5 | 2,351.5 | 18,946 | 3,870 | 5.12 |
| BRC010_ParallelAfterRead | 8 | 77.3 | 62.1 | 1,271.4 | 3,911.2 | 68,644 | 13,748 | 3.08 |
| BRC044..._SelfSplitting | 8 | 25.4 | 20.4 | 715.9 | 2,171.0 | 27,759 | 4,405 | 3.03 |
| BRC048_VT_ChunkSize_10MB | 8 | 25.4 | 20.4 | 738.6 | 2,188.7 | 27,899 | 4,467 | 2.96 |
| BRC060_44_SplitTune | 8 | 19.7 | 15.8 | 569.7 | 1,598.7 | 22,025 | 3,597 | 2.81 |
| BRC062_ParseDouble | 8 | 17.6 | 14.1 | 489.1 | 1,352.3 | 18,593 | 3,027 | 2.76 |
| BRC064_NoExtraString | 8 | 16.3 | 13.1 | 446.6 | 1,208.3 | 15,823 | 2,542 | 2.71 |
| BRC066_Integer | 8 | 14.6 | 11.7 | 408.6 | 1,110.1 | 14,682 | 2,512 | 2.72 |
| BRC068_ParsingWithoutALoop | 8 | 13.6 | 11.0 | 374.3 | 1,031.1 | 14,239 | 2,399 | 2.75 |
Immutability Is Good But Mutations Are Better
/**
* Holds our temperature data without the station, because the
* map already knows that
*/
private static class Temperatures
{
private int min;
private int max;
private long total;
private long count;
public Temperatures(final int temperature)
{
this.min = temperature;
this.max = temperature;
this.total = temperature;
this.count = 1;
}
public Temperatures merge(final int temperature)
{
this.min = Math.min(this.min, temperature);
this.max = Math.max(this.max, temperature);
this.total += temperature;
this.count += 1;
return this;
}
...
}
| Test | C | Time | % | Cycles (B) | Instr (B) | Ctx Switches | CPU Migr | IPC |
|---|---|---|---|---|---|---|---|---|
| BRC001_Baseline | 1 | 124.5 | 100.0 | 459.5 | 2,351.5 | 18,946 | 3,870 | 5.12 |
| BRC010_ParallelAfterRead | 8 | 77.3 | 62.1 | 1,271.4 | 3,911.2 | 68,644 | 13,748 | 3.08 |
| BRC044..._SelfSplitting | 8 | 25.4 | 20.4 | 715.9 | 2,171.0 | 27,759 | 4,405 | 3.03 |
| BRC048_VT_ChunkSize_10MB | 8 | 25.4 | 20.4 | 738.6 | 2,188.7 | 27,899 | 4,467 | 2.96 |
| BRC060_44_SplitTune | 8 | 19.7 | 15.8 | 569.7 | 1,598.7 | 22,025 | 3,597 | 2.81 |
| BRC062_ParseDouble | 8 | 17.6 | 14.1 | 489.1 | 1,352.3 | 18,593 | 3,027 | 2.76 |
| BRC064_NoExtraString | 8 | 16.3 | 13.1 | 446.6 | 1,208.3 | 15,823 | 2,542 | 2.71 |
| BRC066_Integer | 8 | 14.6 | 11.7 | 408.6 | 1,110.1 | 14,682 | 2,512 | 2.72 |
| BRC068_ParsingWithoutALoop | 8 | 13.6 | 11.0 | 374.3 | 1,031.1 | 14,239 | 2,399 | 2.75 |
| BRC070_Mutate | 8 | 12.9 | 10.4 | 351.1 | 989.4 | 10,176 | 1,861 | 2.82 |
| BRC072_Mutate_BetterReader | 8 | 12.9 | 10.3 | 350.2 | 1,016.1 | 10,007 | 1,935 | 2.90 |
Reduce The Churn Even More
String objects are immutableString objects need a conversion from byte databyte until we need a String?-, ; and \n, and 0-9 are the same in UTF-8byte and not String
private static class City {
private int min;
private int max;
private long total;
private long count;
private final byte[] name;
private final int hashCode;
public City(byte[] bytes, int semicolon, int temperature, int hashCode) {
this.name = Arrays.copyOfRange(bytes, 0, semicolon);
this.max = temperature;
this.min = temperature;
this.total = temperature;
this.count = 1;
this.hashCode = hashCode;
}
public City update(int temperature) {
this.min = Math.min(this.min, temperature);
this.max = Math.max(this.max, temperature);
count++;
this.total += temperature;
return this;
}
public City merge(final City city) {
this.min = Math.min(this.min, city.min);
this.max = Math.max(this.max, city.max);
this.total += city.total;
this.count += city.count;
return this;
}
...
}
| Test | C | Time | % | Cycles (B) | Instr (B) | Ctx Switches | CPU Migr | IPC |
|---|---|---|---|---|---|---|---|---|
| BRC001_Baseline | 1 | 124.5 | 100.0 | 459.5 | 2,351.5 | 18,946 | 3,870 | 5.12 |
| BRC010_ParallelAfterRead | 8 | 77.3 | 62.1 | 1,271.4 | 3,911.2 | 68,644 | 13,748 | 3.08 |
| BRC044..._SelfSplitting | 8 | 25.4 | 20.4 | 715.9 | 2,171.0 | 27,759 | 4,405 | 3.03 |
| BRC048_VT_ChunkSize_10MB | 8 | 25.4 | 20.4 | 738.6 | 2,188.7 | 27,899 | 4,467 | 2.96 |
| BRC060_44_SplitTune | 8 | 19.7 | 15.8 | 569.7 | 1,598.7 | 22,025 | 3,597 | 2.81 |
| BRC062_ParseDouble | 8 | 17.6 | 14.1 | 489.1 | 1,352.3 | 18,593 | 3,027 | 2.76 |
| BRC064_NoExtraString | 8 | 16.3 | 13.1 | 446.6 | 1,208.3 | 15,823 | 2,542 | 2.71 |
| BRC066_Integer | 8 | 14.6 | 11.7 | 408.6 | 1,110.1 | 14,682 | 2,512 | 2.72 |
| BRC068_ParsingWithoutALoop | 8 | 13.6 | 11.0 | 374.3 | 1,031.1 | 14,239 | 2,399 | 2.75 |
| BRC070_Mutate | 8 | 12.9 | 10.4 | 351.1 | 989.4 | 10,176 | 1,861 | 2.82 |
| BRC072_Mutate_BetterReader | 8 | 12.9 | 10.3 | 350.2 | 1,016.1 | 10,007 | 1,935 | 2.90 |
| BRC075_Bytes_ConcurrentRead | 8 | 6.3 | 5.1 | 180.6 | 502.1 | 1,159 | 131 | 2.78 |
| BRC075_Bytes_ConcurrentRead | 16 | 3.5 | 2.8 | 89.6 | 501.2 | 1,309 | 91 | 2.81 |
| BRC076_Bytes_ForkJoin_Full | 8 | 11.4 | 9.1 | 324.8 | 851.1 | 8,055 | 1,621 | 2.62 |
| BRC076_Bytes_ForkJoin_Full | 16 | 6.4 | 5.4 | 323.6 | 860.8 | 14,495 | 588 | 2.66 |
Don't split deep
protected Map<City, City> compute() {
// split only when we should
if (taskCount > 1)
{
final List<Mapper> tasks = new ArrayList<>(taskCount);
final long size = this.to - this.from;
final long chunkSize = size / taskCount;
long from = -chunkSize;
long to = 0;
while (to < size) {
from += chunkSize;
to = from + chunkSize;
// when close to the end, make one chunk larger
// than really small... otherwise we lose data
to = (size - to) < chunkSize ? size : to;
tasks.add(new Mapper(filePath, from, to, 1));
}
ForkJoinTask.invokeAll(tasks);
// reduce result
final Map<City, City> cities = new HashMap<>();
for (var t : tasks) {
try {
final var result = t.get();
result.forEach((k, v) ->
cities.compute(k, (_, v2) -> v2 == null ? v : v2.merge(v)));
}
catch (InterruptedException e) {
throw new RuntimeException(e);
}
catch (ExecutionException e) {
throw new RuntimeException(e);
}
}
return cities;
}
else {
return map();
}
}
| Test | C | Time | % | Cycles (B) | Instr (B) | Ctx Switches | CPU Migr | IPC |
|---|---|---|---|---|---|---|---|---|
| BRC001_Baseline | 1 | 124.5 | 100.0 | 459.5 | 2,351.5 | 18,946 | 3,870 | 5.12 |
| BRC010_ParallelAfterRead | 8 | 77.3 | 62.1 | 1,271.4 | 3,911.2 | 68,644 | 13,748 | 3.08 |
| BRC044..._SelfSplitting | 8 | 25.4 | 20.4 | 715.9 | 2,171.0 | 27,759 | 4,405 | 3.03 |
| BRC048_VT_ChunkSize_10MB | 8 | 25.4 | 20.4 | 738.6 | 2,188.7 | 27,899 | 4,467 | 2.96 |
| BRC060_44_SplitTune | 8 | 19.7 | 15.8 | 569.7 | 1,598.7 | 22,025 | 3,597 | 2.81 |
| BRC062_ParseDouble | 8 | 17.6 | 14.1 | 489.1 | 1,352.3 | 18,593 | 3,027 | 2.76 |
| BRC064_NoExtraString | 8 | 16.3 | 13.1 | 446.6 | 1,208.3 | 15,823 | 2,542 | 2.71 |
| BRC066_Integer | 8 | 14.6 | 11.7 | 408.6 | 1,110.1 | 14,682 | 2,512 | 2.72 |
| BRC068_ParsingWithoutALoop | 8 | 13.6 | 11.0 | 374.3 | 1,031.1 | 14,239 | 2,399 | 2.75 |
| BRC070_Mutate | 8 | 12.9 | 10.4 | 351.1 | 989.4 | 10,176 | 1,861 | 2.82 |
| BRC072_Mutate_BetterReader | 8 | 12.9 | 10.3 | 350.2 | 1,016.1 | 10,007 | 1,935 | 2.90 |
| BRC075_Bytes_ConcurrentRead | 8 | 6.3 | 5.1 | 180.6 | 502.1 | 1,159 | 131 | 2.78 |
| BRC075_Bytes_ConcurrentRead | 16 | 3.5 | 2.8 | 89.6 | 501.2 | 1,309 | 91 | 2.81 |
| BRC076_Bytes_ForkJoin_Full | 8 | 11.4 | 9.1 | 324.8 | 851.1 | 8,055 | 1,621 | 2.62 |
| BRC076_Bytes_ForkJoin_Full | 16 | 6.4 | 5.4 | 323.6 | 860.8 | 14,495 | 588 | 2.66 |
| BRC077_Bytes_ForkJoin_1Level | 8 | 11.0 | 8.8 | 319.1 | 851.6 | 7,470 | 1,596 | 2.67 |
| BRC077_Bytes_ForkJoin_1Level | 16 | 6.2 | 5.2 | 321.7 | 865.0 | 13,180 | 353 | 2.69 |
# Before Tuning
Performance counter stats for 'java -Xms2g -Xmx2g org.rschwietzke.parallel.BRC044_ForkJoinPool_SelfSplitting -f data-1000m.txt -wc 0 -mc 1 -t 8':
195,689.07 msec task-clock # 7.527 CPUs utilized
26,837 context-switches # 137.141 /sec
4,385 cpu-migrations # 22.408 /sec
333,597 page-faults # 1.705 K/sec
👉 694,817,257,247 cycles # 3.551 GHz
124,646,585,103 stalled-cycles-frontend # 17.94% frontend cycles idle
👉2,160,182,371,314 instructions # 3.11 insn per cycle
# 0.06 stalled cycles per insn
495,875,578,633 branches # 2.534 G/sec
2,251,135,482 branch-misses # 0.45% of all branches
25.997951402 seconds time elapsed
189.993214000 seconds user
4.836329000 seconds sys
# Basic Tuning Applied
Performance counter stats for 'java -Xms2g -Xmx2g org.rschwietzke.parallel.BRC077_Bytes_ForkJoin_1Level -f data-1000m.txt -wc 0 -mc 1 -t 8':
87,243.92 msec task-clock # 7.665 CPUs utilized
7,340 context-switches # 84.132 /sec
1,535 cpu-migrations # 17.594 /sec
325,138 page-faults # 3.727 K/sec
👉 311,042,959,884 cycles # 3.565 GHz
46,899,550,857 stalled-cycles-frontend # 15.08% frontend cycles idle
👉 841,246,054,158 instructions # 2.70 insn per cycle
# 0.06 stalled cycles per insn
149,906,105,350 branches # 1.718 G/sec
1,826,627,665 branch-misses # 1.22% of all branches
11.381797466 seconds time elapsed
83.807472000 seconds user
3.292472000 seconds sys
| Test | C | Time | % | Cycles (B) | Instr (B) | Ctx Switches | CPU Migr | IPC |
|---|---|---|---|---|---|---|---|---|
| BRC001_Baseline | 1 | 124.5 | 100.0 | 459.5 | 2,351.5 | 18,946 | 3,870 | 5.12 |
| BRC060_44_SplitTune | 8 | 19.7 | 15.8 | 569.7 | 1,598.7 | 22,025 | 3,597 | 2.81 |
| BRC062_ParseDouble | 8 | 17.6 | 14.1 | 489.1 | 1,352.3 | 18,593 | 3,027 | 2.76 |
| BRC064_NoExtraString | 8 | 16.3 | 13.1 | 446.6 | 1,208.3 | 15,823 | 2,542 | 2.71 |
| BRC066_Integer | 8 | 14.6 | 11.7 | 408.6 | 1,110.1 | 14,682 | 2,512 | 2.72 |
| BRC068_ParsingWithoutALoop | 8 | 13.6 | 11.0 | 374.3 | 1,031.1 | 14,239 | 2,399 | 2.75 |
| BRC070_Mutate | 8 | 12.9 | 10.4 | 351.1 | 989.4 | 10,176 | 1,861 | 2.82 |
| BRC076_Bytes_ForkJoin_Full | 8 | 11.4 | 9.1 | 324.8 | 851.1 | 8,055 | 1,621 | 2.62 |
| BRC076_Bytes_ForkJoin_Full | 16 | 6.4 | 5.4 | 323.6 | 860.8 | 14,495 | 588 | 2.66 |
| BRC077_Bytes_..._1Level | 8 | 11.0 | 8.8 | 319.1 | 851.6 | 7,470 | 1,596 | 2.67 |
| BRC077_Bytes_..._1Level | 16 | 6.2 | 5.2 | 321.7 | 865.0 | 13,180 | 353 | 2.69 |
| BRC080_Set | 8 | 7.8 | 6.3 | 220.0 | 640.6 | 714 | 46 | 2.91 |
| BRC080_Set | 16 | 4.2 | 3.4 | 225.6 | 647.2 | 1,577 | 46 | 2.87 |
-Xms5m -Xmx5m -XX:+UseEpsilonGC
The first 80% are not too hard.
Change one thing at a time and measure.