Initial version for thomaswue with Oracle GraalVM Native Image
* 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 <alfonso.peterssen@oracle.com>
This commit is contained in:
		
				
					committed by
					
						 GitHub
						GitHub
					
				
			
			
				
	
			
			
			
						parent
						
							093bd35c44
						
					
				
				
					commit
					a53aa2e6fd
				
			
							
								
								
									
										21
									
								
								additional_build_step_thomaswue.sh
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										21
									
								
								additional_build_step_thomaswue.sh
									
									
									
									
									
										Executable file
									
								
							| @@ -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 | ||||||
							
								
								
									
										29
									
								
								calculate_average_thomaswue.sh
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										29
									
								
								calculate_average_thomaswue.sh
									
									
									
									
									
										Executable file
									
								
							| @@ -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 | ||||||
|  |  | ||||||
							
								
								
									
										212
									
								
								src/main/java/dev/morling/onebrc/CalculateAverage_thomaswue.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										212
									
								
								src/main/java/dev/morling/onebrc/CalculateAverage_thomaswue.java
									
									
									
									
									
										Normal file
									
								
							| @@ -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<HashMap<String, Result>> allResults = IntStream.range(0, chunks.length - 1).mapToObj(chunkIndex -> { | ||||||
|  |             HashMap<String, Result> 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<String, Result> result = allResults.getFirst(); | ||||||
|  |         for (int i = 1; i < allResults.size(); ++i) { | ||||||
|  |             for (Map.Entry<String, Result> 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<String, Result> 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; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
		Reference in New Issue
	
	Block a user