From a53aa2e6fdbcc74e0c6a9a308f0a8c3507b0e06a Mon Sep 17 00:00:00 2001 From: Thomas Wuerthinger Date: Sat, 6 Jan 2024 10:55:07 +0100 Subject: [PATCH] Initial version for thomaswue with Oracle GraalVM Native Image MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Initial version. * Make PGO feature optional off-by-default. Needs PGO_MODE environment variable to be set. Add -O3 -march=native tuning flags for better performance. * Adjust script to be more quiet. * Adjust max city length. Fix an issue when accumulating results. * Tune thomaswue submission. mmap the entire file, use Unsafe directly instead of ByteBuffer, avoid byte[] copies. These tricks give a ~30% speedup, over an already fast implementation. * Optimize parsing of numbers based on specific given constraints. * Fix for segment calculation for case of very small input. * Minor shell script fixes. * Separate out build step into file additional_build_step_thomaswue.sh, simplify run script and remove PGO option for now. * Minor corrections to the run script. --------- Co-authored-by: Alfonso² Peterssen --- additional_build_step_thomaswue.sh | 21 ++ calculate_average_thomaswue.sh | 29 +++ .../onebrc/CalculateAverage_thomaswue.java | 212 ++++++++++++++++++ 3 files changed, 262 insertions(+) create mode 100755 additional_build_step_thomaswue.sh create mode 100755 calculate_average_thomaswue.sh create mode 100644 src/main/java/dev/morling/onebrc/CalculateAverage_thomaswue.java diff --git a/additional_build_step_thomaswue.sh b/additional_build_step_thomaswue.sh new file mode 100755 index 0000000..ab2f365 --- /dev/null +++ b/additional_build_step_thomaswue.sh @@ -0,0 +1,21 @@ +#!/bin/sh +# +# Copyright 2023 The original authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +source "$HOME/.sdkman/bin/sdkman-init.sh" +sdk use java 21.0.1-graal 1>&2 +NATIVE_IMAGE_OPTS="--gc=epsilon -O3 -march=native --enable-preview" +native-image $NATIVE_IMAGE_OPTS -cp target/average-1.0.0-SNAPSHOT.jar -o image_calculateaverage_thomaswue dev.morling.onebrc.CalculateAverage_thomaswue diff --git a/calculate_average_thomaswue.sh b/calculate_average_thomaswue.sh new file mode 100755 index 0000000..d875f8a --- /dev/null +++ b/calculate_average_thomaswue.sh @@ -0,0 +1,29 @@ +#!/bin/sh +# +# Copyright 2023 The original authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + + +if [ -f ./image_calculateaverage_thomaswue ]; then + echo "Picking up existing native image, delete the file to select JVM mode." 1>&2 + time ./image_calculateaverage_thomaswue +else + source "$HOME/.sdkman/bin/sdkman-init.sh" + sdk use java 21.0.1-graal 1>&2 + JAVA_OPTS="--enable-preview" + echo "Chosing to run the app in JVM mode as no native image was found, use additional_build_step_thomaswue.sh to generate." 1>&2 + time java $JAVA_OPTS --class-path target/average-1.0.0-SNAPSHOT.jar dev.morling.onebrc.CalculateAverage_thomaswue +fi + diff --git a/src/main/java/dev/morling/onebrc/CalculateAverage_thomaswue.java b/src/main/java/dev/morling/onebrc/CalculateAverage_thomaswue.java new file mode 100644 index 0000000..2a3d3e1 --- /dev/null +++ b/src/main/java/dev/morling/onebrc/CalculateAverage_thomaswue.java @@ -0,0 +1,212 @@ +/* + * Copyright 2023 The original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package dev.morling.onebrc; + +import sun.misc.Unsafe; + +import java.io.IOException; +import java.lang.foreign.Arena; +import java.lang.reflect.Field; +import java.nio.channels.FileChannel; +import java.nio.channels.FileChannel.MapMode; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; +import java.util.stream.IntStream; + +public class CalculateAverage_thomaswue { + private static final String FILE = "./measurements.txt"; + + // Holding the current result for a single city. + private static class Result { + int min; + int max; + long sum; + int count; + final long nameAddress; + final int nameLength; + + private Result(long nameAddress, int nameLength, int value) { + this.nameAddress = nameAddress; + this.nameLength = nameLength; + this.min = value; + this.max = value; + this.sum = value; + this.count = 1; + } + + public String toString() { + return round(((double) min) / 10.0) + "/" + round((((double) sum) / 10.0) / count) + "/" + round(((double) max) / 10.0); + } + + private static double round(double value) { + return Math.round(value * 10.0) / 10.0; + } + + // Accumulate another result into this one. + private void add(Result other) { + min = Math.min(min, other.min); + max = Math.max(max, other.max); + sum += other.sum; + count += other.count; + } + } + + public static void main(String[] args) throws IOException { + // Calculate input segments. + int numberOfChunks = Runtime.getRuntime().availableProcessors(); + long[] chunks = getSegments(numberOfChunks); + + // Parallel processing of segments. + List> allResults = IntStream.range(0, chunks.length - 1).mapToObj(chunkIndex -> { + HashMap cities = HashMap.newHashMap(1 << 10); + Result[] results = new Result[1 << 14]; + parseLoop(chunks[chunkIndex], chunks[chunkIndex + 1], results, cities); + return cities; + }).parallel().toList(); + + // Accumulate results sequentially. + HashMap result = allResults.getFirst(); + for (int i = 1; i < allResults.size(); ++i) { + for (Map.Entry entry : allResults.get(i).entrySet()) { + Result current = result.get(entry.getKey()); + if (current != null) { + current.add(entry.getValue()); + } + else { + result.put(entry.getKey(), entry.getValue()); + } + } + } + + // Final output. + System.out.println(new TreeMap<>(result)); + } + + private static final Unsafe UNSAFE = initUnsafe(); + + private static Unsafe initUnsafe() { + try { + Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe"); + theUnsafe.setAccessible(true); + return (Unsafe) theUnsafe.get(Unsafe.class); + } + catch (NoSuchFieldException | IllegalAccessException e) { + throw new RuntimeException(e); + } + } + + static boolean unsafeEquals(long aStart, long aLength, long bStart, long bLength) { + if (aLength != bLength) { + return false; + } + for (int i = 0; i < aLength; ++i) { + if (UNSAFE.getByte(aStart + i) != UNSAFE.getByte(bStart + i)) { + return false; + } + } + return true; + } + + private static void parseLoop(long chunkStart, long chunkEnd, Result[] results, HashMap cities) { + long scanPtr = chunkStart; + byte b; + while (scanPtr < chunkEnd) { + long nameAddress = scanPtr; + + int hash = UNSAFE.getByte(scanPtr++); + while ((b = UNSAFE.getByte(scanPtr++)) != ';') { + hash += b; + hash += hash << 10; + hash ^= hash >> 6; + } + + int nameLength = (int) (scanPtr - 1 - nameAddress); + hash = hash & (results.length - 1); + + int number; + byte sign = UNSAFE.getByte(scanPtr++); + if (sign == '-') { + number = UNSAFE.getByte(scanPtr++) - '0'; + if ((b = UNSAFE.getByte(scanPtr++)) != '.') { + number = number * 10 + (b - '0'); + scanPtr++; + } + number = number * 10 + (UNSAFE.getByte(scanPtr++) - '0'); + number = -number; + } + else { + number = sign - '0'; + if ((b = UNSAFE.getByte(scanPtr++)) != '.') { + number = number * 10 + (b - '0'); + scanPtr++; + } + number = number * 10 + (UNSAFE.getByte(scanPtr++) - '0'); + } + + while (true) { + Result existingResult = results[hash]; + if (existingResult == null) { + Result r = new Result(nameAddress, nameLength, number); + results[hash] = r; + byte[] bytes = new byte[nameLength]; + UNSAFE.copyMemory(null, nameAddress, bytes, Unsafe.ARRAY_BYTE_BASE_OFFSET, nameLength); + cities.put(new String(bytes, StandardCharsets.UTF_8), r); + break; + } + else if (unsafeEquals(existingResult.nameAddress, existingResult.nameLength, nameAddress, nameLength)) { + existingResult.min = Math.min(existingResult.min, number); + existingResult.max = Math.max(existingResult.max, number); + existingResult.sum += number; + existingResult.count++; + break; + } + else { + // Collision error, try next. + hash = (hash + 1) & (results.length - 1); + } + } + + // Skip new line. + scanPtr++; + } + } + + private static long[] getSegments(int numberOfChunks) throws IOException { + try (var fileChannel = FileChannel.open(Path.of(FILE), StandardOpenOption.READ)) { + long fileSize = fileChannel.size(); + long segmentSize = (fileSize + numberOfChunks - 1) / numberOfChunks; + long[] chunks = new long[numberOfChunks + 1]; + long mappedAddress = fileChannel.map(MapMode.READ_ONLY, 0, fileSize, Arena.global()).address(); + chunks[0] = mappedAddress; + long endAddress = mappedAddress + fileSize; + for (int i = 1; i < numberOfChunks; ++i) { + long chunkAddress = mappedAddress + i * segmentSize; + // Align to first row start. + while (chunkAddress < endAddress && UNSAFE.getByte(chunkAddress++) != '\n') { + // nop + } + chunks[i] = Math.min(chunkAddress, endAddress); + } + chunks[numberOfChunks] = endAddress; + return chunks; + } + } +}