diff --git a/prepare_royvanrijn.sh b/prepare_royvanrijn.sh index 2088b7b..ba89535 100755 --- a/prepare_royvanrijn.sh +++ b/prepare_royvanrijn.sh @@ -22,7 +22,7 @@ sdk use java 21.0.1-graal 1>&2 if [ ! -f target/CalculateAverage_royvanrijn_image ]; then JAVA_OPTS="--enable-preview -dsa" - NATIVE_IMAGE_OPTS="--gc=epsilon -Ob -O3 -march=native --strict-image-heap $JAVA_OPTS" + NATIVE_IMAGE_OPTS="--initialize-at-build-time=dev.morling.onebrc.CalculateAverage_royvanrijn --gc=epsilon -Ob -O3 -march=native --strict-image-heap $JAVA_OPTS" native-image $NATIVE_IMAGE_OPTS -cp target/average-1.0.0-SNAPSHOT.jar -o target/CalculateAverage_royvanrijn_image dev.morling.onebrc.CalculateAverage_royvanrijn fi diff --git a/src/main/java/dev/morling/onebrc/CalculateAverage_royvanrijn.java b/src/main/java/dev/morling/onebrc/CalculateAverage_royvanrijn.java index 307833f..b392e58 100644 --- a/src/main/java/dev/morling/onebrc/CalculateAverage_royvanrijn.java +++ b/src/main/java/dev/morling/onebrc/CalculateAverage_royvanrijn.java @@ -15,17 +15,15 @@ */ package dev.morling.onebrc; -import java.io.IOException; import java.lang.foreign.Arena; import java.lang.reflect.Field; import java.nio.channels.FileChannel; import java.nio.charset.StandardCharsets; import java.nio.file.Path; import java.nio.file.StandardOpenOption; -import java.util.HashMap; import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Collectors; -import java.util.stream.IntStream; import sun.misc.Unsafe; @@ -55,216 +53,500 @@ import sun.misc.Unsafe; * Remove writing to buffer: 1335 ms * Optimized collecting at the end: 1310 ms * Adding a lot of comments: priceless + * Changed to flyweight byte[]: 1290 ms (adds even more Unsafe, was initially slower, now faster) + * More LOC now parallel: 1260 ms (moved more to processMemoryArea, recombining in ConcurrentHashMap) + * Storing only the address: 1240 ms (this is now faster, tried before, was slower) + * Unrolling scan-loop: 1200 ms (seems to help, perhaps even more on target machine) + * Adding more readable reader: 1300 ms (scores got worse on target machine anyway) * - * Big thanks to Francesco Nigro, Thomas Wuerthinger, Quan Anh Mai for ideas. + * I've ditched my M2 for an older x86-64 MacBook, this allows me to run `perf` and I'm trying to get lower numbers by trail and error. + * + * Big thanks to Francesco Nigro, Thomas Wuerthinger, Quan Anh Mai and many others for ideas. * * Follow me at: @royvanrijn */ public class CalculateAverage_royvanrijn { private static final String FILE = "./measurements.txt"; + // private static final String FILE = "src/test/resources/samples/measurements-1.txt"; private static final Unsafe UNSAFE = initUnsafe(); - private static Unsafe initUnsafe() { - try { - final Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe"); - theUnsafe.setAccessible(true); - return (Unsafe) theUnsafe.get(Unsafe.class); - } - catch (NoSuchFieldException | IllegalAccessException e) { - throw new RuntimeException(e); - } - } - - public static void main(String[] args) throws Exception { - // Calculate input segments. - final int numberOfChunks = Runtime.getRuntime().availableProcessors(); - final long[] chunks = getSegments(numberOfChunks); - - final Map measurements = HashMap.newHashMap(1 << 10); - IntStream.range(0, chunks.length - 1) - .mapToObj(chunkIndex -> processMemoryArea(chunks[chunkIndex], chunks[chunkIndex + 1])) - .parallel() - .forEachOrdered(repo -> { // make sure it's ordered, no concurrent map - for (Entry entry : repo) { - if (entry != null) - measurements.merge(turnLongArrayIntoString(entry.data, entry.length), entry, Entry::mergeWith); - } - }); - - System.out.print("{" + - measurements.entrySet().stream().sorted(Map.Entry.comparingByKey()).map(Object::toString).collect(Collectors.joining(", "))); - System.out.println("}"); - } + // Twice the processors, smoothens things out. + private static final int PROCESSORS = Runtime.getRuntime().availableProcessors(); /** - * Simpler way to get the segments and launch parallel processing by thomaswue + * Flyweight entry in a byte[], max 128 bytes. + * + * long: sum + * int: min + * int: max + * int: count + * byte: length + * byte[]: cityname */ - private static long[] getSegments(final int numberOfChunks) throws IOException { - try (var fileChannel = FileChannel.open(Path.of(FILE), StandardOpenOption.READ)) { - final long fileSize = fileChannel.size(); - final long segmentSize = (fileSize + numberOfChunks - 1) / numberOfChunks; - final long[] chunks = new long[numberOfChunks + 1]; - final long mappedAddress = fileChannel.map(FileChannel.MapMode.READ_ONLY, 0, fileSize, Arena.global()).address(); - chunks[0] = mappedAddress; - final 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; - } - } + // ------------------------------------------------------------------------ + private static final int ENTRY_LENGTH = (Unsafe.ARRAY_BYTE_BASE_OFFSET); + private static final int ENTRY_SUM = (ENTRY_LENGTH + Byte.BYTES); + private static final int ENTRY_MIN = (ENTRY_SUM + Long.BYTES); + private static final int ENTRY_MAX = (ENTRY_MIN + Integer.BYTES); + private static final int ENTRY_COUNT = (ENTRY_MAX + Integer.BYTES); + private static final int ENTRY_NAME = (ENTRY_COUNT + Integer.BYTES); + private static final int ENTRY_NAME_8 = ENTRY_NAME + 8; + private static final int ENTRY_NAME_16 = ENTRY_NAME + 16; - // This is where I store the hashtable entry data in the "hot loop" - // The long[] contains the name in bytes (yeah, confusing) - // I've tried flyweight-ing, carrying all the data in a single byte[], - // where you offset type-indices: min:int,max:int,count:int,etc. - // - // The performance was just a little worse than this simple class. - static final class Entry { - - private int min, max, count; - private byte length; - private long sum; - private final long[] data; - - Entry(final long[] data, byte length, int temp) { - this.data = data; - this.length = length; - this.min = temp; - this.max = temp; - this.sum = temp; - this.count = 1; - } - - public void updateWith(int measurement) { - min = Math.min(min, measurement); - max = Math.max(max, measurement); - sum += measurement; - count++; - } - - public Entry mergeWith(Entry entry) { - min = Math.min(min, entry.min); - max = Math.max(max, entry.max); - sum += entry.sum; - count += entry.count; - return this; - } - - public String toString() { - return round(min) + "/" + round((1.0 * sum) / count) + "/" + round(max); - } - - private static double round(double value) { - return Math.round(value) / 10.0; - } - } - - // Only parse the String at the final end, when we have only the needed entries left that we need to output: - private static String turnLongArrayIntoString(final long[] data, final int length) { - // Create our target byte[] - final byte[] bytes = new byte[length]; - // The power of magic allows us to just copy the memory in there. - UNSAFE.copyMemory(data, Unsafe.ARRAY_LONG_BASE_OFFSET, bytes, Unsafe.ARRAY_BYTE_BASE_OFFSET, length); - // And construct a String() - return new String(bytes, StandardCharsets.UTF_8); - } - - private static Entry createNewEntry(final long fromAddress, final int lengthLongs, final byte lengthBytes, final int temp) { - // Make a copy of our working buffer, store this in a new Entry: - final long[] bufferCopy = new long[lengthLongs]; - // Just copy everything over, bytes into the long[] - UNSAFE.copyMemory(null, fromAddress, bufferCopy, Unsafe.ARRAY_BYTE_BASE_OFFSET, lengthBytes); - return new Entry(bufferCopy, lengthBytes, temp); - } - - private static final int TABLE_SIZE = 1 << 19; + private static final int ENTRY_BASESIZE_WHITESPACE = ENTRY_NAME + 7; // with enough empty bytes to fill a long + // ------------------------------------------------------------------------ + private static final int PREMADE_MAX_SIZE = 1 << 5; // pre-initialize some entries in memory, keep them close + private static final int PREMADE_ENTRIES = 512; // amount of pre-created entries we should use + private static final int TABLE_SIZE = 1 << 19; // large enough for the contest. private static final int TABLE_MASK = (TABLE_SIZE - 1); - private static Entry[] processMemoryArea(final long fromAddress, final long toAddress) { + public static void main(String[] args) throws Exception { - int packedBytes; - long hash; - long ptr = fromAddress; - long word; - long mask; + // Calculate input segments. + final FileChannel fileChannel = FileChannel.open(Path.of(FILE), StandardOpenOption.READ); + final long fileSize = fileChannel.size(); + final long segmentSize = (fileSize + PROCESSORS - 1) / PROCESSORS; + final long mapAddress = fileChannel.map(FileChannel.MapMode.READ_ONLY, 0, fileSize, Arena.global()).address(); - final Entry[] table = new Entry[TABLE_SIZE]; + final Thread[] parallelThreads = new Thread[PROCESSORS - 1]; - // Go from start to finish address through the bytes: - while (ptr < toAddress) { + // This is where the entries will land: + final ConcurrentHashMap measurements = new ConcurrentHashMap(1 << 10); - final long startAddress = ptr; + // We create separate threads for twice the amount of processors. + long lastAddress = mapAddress; + final long endOfFile = mapAddress + fileSize; + for (int i = 0; i < PROCESSORS - 1; ++i) { - packedBytes = 1; + final long fromAddress = lastAddress; + final long toAddress = Math.min(endOfFile, fromAddress + segmentSize); + + final Thread thread = new Thread(() -> { + // The actual work is done here: + final byte[][] table = processMemoryArea(fromAddress, toAddress, fromAddress == mapAddress); + + for (byte[] entry : table) { + if (entry != null) { + measurements.merge(entryToName(entry), entry, CalculateAverage_royvanrijn::mergeEntry); + } + } + }); + thread.start(); // start a.s.a.p. + parallelThreads[i] = thread; + lastAddress = toAddress; + } + + // Use the current thread for the part of memory: + final byte[][] table = processMemoryArea(lastAddress, mapAddress + fileSize, false); + + for (byte[] entry : table) { + if (entry != null) { + measurements.merge(entryToName(entry), entry, CalculateAverage_royvanrijn::mergeEntry); + } + } + // Wait for all threads to finish: + for (Thread thread : parallelThreads) { + // Can we implement work-stealing? Not sure how... + thread.join(); + } + + // If we don't reach start of file, + System.out.print("{" + + measurements.entrySet().stream().sorted(Map.Entry.comparingByKey()) + .map(entry -> entry.getKey() + '=' + entryValuesToString(entry.getValue())) + .collect(Collectors.joining(", "))); + System.out.println("}"); + + // System.out.println(measurements.entrySet().stream().mapToLong(e -> UNSAFE.getInt(e.getValue(), ENTRY_COUNT + Unsafe.ARRAY_BYTE_BASE_OFFSET)).sum()); + } + + private static byte[] fillEntry(final byte[] entry, final long fromAddress, final int length, final int temp) { + UNSAFE.putLong(entry, ENTRY_SUM, temp); + UNSAFE.putInt(entry, ENTRY_MIN, temp); + UNSAFE.putInt(entry, ENTRY_MAX, temp); + UNSAFE.putInt(entry, ENTRY_COUNT, 1); + UNSAFE.putByte(entry, ENTRY_LENGTH, (byte) length); + UNSAFE.copyMemory(null, fromAddress, entry, ENTRY_NAME, length); + return entry; + } + + public static void updateEntry(final byte[] entry, final int temp) { + + int entryMin = UNSAFE.getInt(entry, ENTRY_MIN); + int entryMax = UNSAFE.getInt(entry, ENTRY_MAX); + + entryMin = Math.min(temp, entryMin); + entryMax = Math.max(temp, entryMax); + + long entrySum = UNSAFE.getLong(entry, ENTRY_SUM) + temp; + int entryCount = UNSAFE.getInt(entry, ENTRY_COUNT) + 1; + + UNSAFE.putInt(entry, ENTRY_MIN, entryMin); + UNSAFE.putInt(entry, ENTRY_MAX, entryMax); + UNSAFE.putInt(entry, ENTRY_COUNT, entryCount); + UNSAFE.putLong(entry, ENTRY_SUM, entrySum); + } + + public static byte[] mergeEntry(final byte[] entry, final byte[] merge) { + + long sum = UNSAFE.getLong(merge, ENTRY_SUM); + final int mergeMin = UNSAFE.getInt(merge, ENTRY_MIN); + final int mergeMax = UNSAFE.getInt(merge, ENTRY_MAX); + int count = UNSAFE.getInt(merge, ENTRY_COUNT); + + sum += UNSAFE.getLong(entry, ENTRY_SUM); + int entryMin = UNSAFE.getInt(entry, ENTRY_MIN); + int entryMax = UNSAFE.getInt(entry, ENTRY_MAX); + count += UNSAFE.getInt(entry, ENTRY_COUNT); + + entryMin = Math.min(entryMin, mergeMin); + entryMax = Math.max(entryMax, mergeMax); + + UNSAFE.putLong(entry, ENTRY_SUM, sum); + UNSAFE.putInt(entry, ENTRY_MIN, entryMin); + UNSAFE.putInt(entry, ENTRY_MAX, entryMax); + UNSAFE.putInt(entry, ENTRY_COUNT, count); + return entry; + } + + private static String entryToName(final byte[] entry) { + // Get the length from memory: + int length = UNSAFE.getByte(entry, ENTRY_LENGTH); + + byte[] name = new byte[length]; + UNSAFE.copyMemory(entry, ENTRY_NAME, name, Unsafe.ARRAY_BYTE_BASE_OFFSET, length); + + // Create a new String with the existing byte[]: + return new String(name, StandardCharsets.UTF_8); + } + + private static String entryValuesToString(final byte[] entry) { + return round(UNSAFE.getInt(entry, ENTRY_MIN)) + + "/" + + round((1.0 * UNSAFE.getLong(entry, ENTRY_SUM)) / + UNSAFE.getInt(entry, ENTRY_COUNT)) + + "/" + + round(UNSAFE.getInt(entry, ENTRY_MAX)); + } + + // Print a piece of memory: + // For debug. + private static String printMemory(final Object target, final long address, int length) { + String result = ""; + for (int i = 0; i < length; i++) { + result += (char) UNSAFE.getByte(target, address + i); + } + return result; + } + + // Print a piece of memory: + // For debug. + private static String printMemory(final long value, int length) { + String result = ""; + for (int i = 0; i < length; i++) { + result += (char) ((value >> (i << 3)) & 0xFF); + } + return result; + } + + private static double round(final double value) { + return Math.round(value) / 10.0; + } + + private static final class Reader { + + private long ptr; + private long delimiterMask; + private long lastRead; + private long lastReadMinOne; + + private long hash; + private long entryStart; + private long entryDelimiter; + + private final long endAddress; + + Reader(final long startAddress, final long endAddress, final boolean isFileStart) { + + this.ptr = startAddress; + this.endAddress = endAddress; + + // Adjust start to next delimiter: + if (!isFileStart) { + ptr--; + while (ptr < endAddress) { + if (UNSAFE.getByte(ptr++) == '\n') { + break; + } + } + } + } + + private void processStart() { hash = 0; - word = UNSAFE.getLong(ptr); - mask = getDelimiterMask(word); + entryStart = ptr; + } - // Removed writing to a buffer here, why would we, we know the address and we'll need to check there anyway. - while (mask == 0) { - // If the mask is zero, we have no ';' - packedBytes++; - // So we continue building the hash: - hash ^= word; - ptr += 8; + private boolean hasNext() { + return (ptr < endAddress); + } - // And getting a new value and mask: - word = UNSAFE.getLong(ptr); - mask = getDelimiterMask(word); - } + private static final long DELIMITER_MASK = 0x3B3B3B3B3B3B3B3BL; + private boolean readFirst() { + lastRead = UNSAFE.getLong(ptr); + + final long match = lastRead ^ DELIMITER_MASK; + delimiterMask = (match - 0x0101010101010101L) & (~match & 0x8080808080808080L); + + return delimiterMask == 0; + } + + private boolean readNext() { + lastReadMinOne = lastRead; + return readFirst(); + } + + private void processName() { + hash ^= lastRead; + ptr += 8; + } + + private int processEndAndGetTemperature() { + processFinalBytes(); + + finalizeHash(); + finalizeDelimiter(); + + return readTemperature(); + } + + private void processFinalBytes() { + // Shift and read the last bytes: + lastRead &= ((delimiterMask >>> 7) - 1); + } + + private void finalizeHash() { + // Finalize hash: + hash ^= lastRead; + hash ^= hash >> 32; + hash ^= hash >> 17; // extra entropy + } + + private void finalizeDelimiter() { // Found delimiter: - final int delimiterByte = Long.numberOfTrailingZeros(mask); - final long delimiterAddress = ptr + (delimiterByte >> 3); + entryDelimiter = ptr + (Long.numberOfTrailingZeros(delimiterMask) >> 3); + } - // Finish the masks and hash: - final long partialWord = word & ((mask >>> 7) - 1); - hash ^= partialWord; + private static final long DOT_BITS = 0x10101000; + private static final long MAGIC_MULTIPLIER = (100 * 0x1000000 + 10 * 0x10000 + 1); - // Read a long value from memory starting from the delimiter + 1, the number part: - final long numberBytes = UNSAFE.getLong(delimiterAddress + 1); - final long invNumberBytes = ~numberBytes; + // Awesome idea of merykitty: + private int readTemperature() { + // This is the number part: X.X, -X.X, XX.x or -XX.X + long numberBytes = UNSAFE.getLong(entryDelimiter + 1); + long invNumberBytes = ~numberBytes; - // Adjust our pointer - final int decimalSepPos = Long.numberOfTrailingZeros(invNumberBytes & DOT_BITS); - ptr = delimiterAddress + (decimalSepPos >> 3) + 4; + int dotPosition = Long.numberOfTrailingZeros(invNumberBytes & DOT_BITS); - // Calculate the final hash and index of the table: - int intHash = (int) (hash ^ (hash >> 32)); - intHash = intHash ^ (intHash >> 17); - int index = intHash & TABLE_MASK; + // Update the pointer here, bit awkward, but we have all the data + ptr = entryDelimiter + (dotPosition >> 3) + 4; - // Find or insert the entry: - while (true) { - Entry tableEntry = table[index]; - if (tableEntry == null) { - final int temp = extractTemp(decimalSepPos, invNumberBytes, numberBytes); - // Create a new entry: - final byte length = (byte) (delimiterAddress - startAddress); - table[index] = createNewEntry(startAddress, packedBytes, length, temp); - break; + int min28 = (28 - dotPosition); + // Calculates the sign + final long signed = (invNumberBytes << 59) >> 63; + final long minusFilter = ~(signed & 0xFF); + // Use the pre-calculated decimal position to adjust the values + long digits = ((numberBytes & minusFilter) << min28) & 0x0F000F0F00L; + // Multiply by a magic (100 * 0x1000000 + 10 * 0x10000 + 1), to get the result + final long absValue = ((digits * MAGIC_MULTIPLIER) >>> 32) & 0x3FF; + // And perform abs() + return (int) ((absValue + signed) ^ signed); // non-patented method of doing the same trick + } + + private boolean matchesEntryFull(final byte[] entry) { + int longs = (int) (entryDelimiter - entryStart) >> 3; + int step = 0; + for (int i = 0; i < longs - 2; i++) { + if (UNSAFE.getLong(entryStart + step) != UNSAFE.getLong(entry, ENTRY_NAME + step)) { + return false; } - // Don't bother re-checking things here like hash or length. - // we'll need to check the content anyway if it's a hit, which is most times - else if (memoryEqualsEntry(startAddress, tableEntry.data, partialWord, packedBytes)) { - // temperature, you're not temporary my friend - final int temp = extractTemp(decimalSepPos, invNumberBytes, numberBytes); - // No differences, same entry: - tableEntry.updateWith(temp); - break; - } - // Move to the next in the table, linear probing: - index = (index + 1) & TABLE_MASK; + step += 8; } + if (lastReadMinOne != UNSAFE.getLong(entry, (ENTRY_NAME_8) + step)) { + return false; + } + if (lastRead != UNSAFE.getLong(entry, (ENTRY_NAME_16) + step)) { + return false; + } + return true; + + } + + private boolean matchesEntryMedium(final byte[] entry) { + if (UNSAFE.getLong(entryStart) != UNSAFE.getLong(entry, ENTRY_NAME)) { + return false; + } + if (lastReadMinOne != UNSAFE.getLong(entry, ENTRY_NAME_8)) { + return false; + } + if (lastRead != UNSAFE.getLong(entry, ENTRY_NAME_16)) { + return false; + } + return true; + } + + private boolean matchesEntryShort(final byte[] entry) { + if (lastReadMinOne != UNSAFE.getLong(entry, ENTRY_NAME)) { + return false; + } + if (lastRead != UNSAFE.getLong(entry, ENTRY_NAME_8)) { + return false; + } + return true; + } + + private boolean matchesEnding(final byte[] entry) { + return lastRead == UNSAFE.getLong(entry, ENTRY_NAME); + } + + private int length() { + return (int) (entryDelimiter - entryStart); + + } + + } + + private static byte[][] processMemoryArea(final long startAddress, final long endAddress, boolean isFileStart) { + + final byte[][] table = new byte[TABLE_SIZE][]; + final byte[][] preConstructedEntries = new byte[PREMADE_ENTRIES][ENTRY_BASESIZE_WHITESPACE + PREMADE_MAX_SIZE]; + + final Reader reader = new Reader(startAddress, endAddress, isFileStart); + + byte[] entry; + int entryCount = 0; + + // Find the correct starting position + while (reader.hasNext()) { + + reader.processStart(); + + if (!reader.readFirst()) { + int temperature = reader.processEndAndGetTemperature(); + + // Find or insert the entry: + int index = (int) (reader.hash & TABLE_MASK); + while (true) { + entry = table[index]; + if (entry == null) { + int length = reader.length(); + byte[] entryBytes = (entryCount < PREMADE_ENTRIES) ? preConstructedEntries[entryCount++] + : new byte[ENTRY_BASESIZE_WHITESPACE + length]; + table[index] = fillEntry(entryBytes, reader.entryStart, length, temperature); + break; + } + else if (reader.matchesEnding(entry)) { + updateEntry(entry, temperature); + break; + } + else { + // Move to the next index + index = (index + 1) & TABLE_MASK; + } + } + } + else { + reader.processName(); + + if (!reader.readNext()) { + + int temperature = reader.processEndAndGetTemperature(); + + // Find or insert the entry: + int index = (int) (reader.hash & TABLE_MASK); + while (true) { + entry = table[index]; + if (entry == null) { + int length = reader.length(); + byte[] entryBytes = (entryCount < PREMADE_ENTRIES) ? preConstructedEntries[entryCount++] + : new byte[ENTRY_BASESIZE_WHITESPACE + length]; + table[index] = fillEntry(entryBytes, reader.entryStart, length, temperature); + break; + } + else if (reader.matchesEntryShort(entry)) { + updateEntry(entry, temperature); + break; + } + else { + // Move to the next index + index = (index + 1) & TABLE_MASK; + } + } + } + else { + reader.processName(); + + if (!reader.readNext()) { + int temperature = reader.processEndAndGetTemperature(); + + // Find or insert the entry: + int index = (int) (reader.hash & TABLE_MASK); + while (true) { + entry = table[index]; + if (entry == null) { + int length = reader.length(); + byte[] entryBytes = (entryCount < PREMADE_ENTRIES) ? preConstructedEntries[entryCount++] + : new byte[ENTRY_BASESIZE_WHITESPACE + length]; + table[index] = fillEntry(entryBytes, reader.entryStart, length, temperature); + break; + } + else if (reader.matchesEntryMedium(entry)) { + updateEntry(entry, temperature); + break; + } + else { + // Move to the next index + index = (index + 1) & TABLE_MASK; + } + } + + } + else { + + reader.processName(); + while (reader.readNext()) { + reader.processName(); + } + + int temperature = reader.processEndAndGetTemperature(); + + // Find or insert the entry: + int index = (int) (reader.hash & TABLE_MASK); + while (true) { + entry = table[index]; + if (entry == null) { + int length = reader.length(); + byte[] entryBytes = (length < PREMADE_MAX_SIZE && entryCount < PREMADE_ENTRIES) ? preConstructedEntries[entryCount++] + : new byte[ENTRY_BASESIZE_WHITESPACE + length]; // with enough room + table[index] = fillEntry(entryBytes, reader.entryStart, length, temperature); + break; + } + else if (reader.matchesEntryFull(entry)) { + updateEntry(entry, temperature); + break; + } + else { + // Move to the next index + index = (index + 1) & TABLE_MASK; + } + } + } + } + } + } return table; } @@ -277,52 +559,16 @@ public class CalculateAverage_royvanrijn { * ---------------- BETTER SOFTWARE, FASTER -- * * https://www.openvalue.eu/ - * - * Made you look. - * */ - private static final long DOT_BITS = 0x10101000; - private static final long MAGIC_MULTIPLIER = (100 * 0x1000000 + 10 * 0x10000 + 1); - - private static int extractTemp(final int decimalSepPos, final long invNumberBits, final long numberBits) { - // Awesome idea of merykitty: - int min28 = (28 - decimalSepPos); - // Calculates the sign - final long signed = (invNumberBits << 59) >> 63; - final long minusFilter = ~(signed & 0xFF); - // Use the pre-calculated decimal position to adjust the values - final long digits = ((numberBits & minusFilter) << min28) & 0x0F000F0F00L; - // Multiply by a magic (100 * 0x1000000 + 10 * 0x10000 + 1), to get the result - final long absValue = ((digits * MAGIC_MULTIPLIER) >>> 32) & 0x3FF; - // And perform abs() - final int temp = (int) ((absValue + signed) ^ signed); // non-patented method of doing the same trick - return temp; - } - - private static final long SEPARATOR_PATTERN = 0x3B3B3B3B3B3B3B3BL; - - // Takes a long and finds the bytes where this exact pattern is present. - // Cool bit manipulation technique: SWAR (SIMD as a Register). - private static long getDelimiterMask(final long word) { - final long match = word ^ SEPARATOR_PATTERN; - return (match - 0x0101010101010101L) & (~match & 0x8080808080808080L); - // I've put some brackets separating the first and second part, this is faster. - // Now they run simultaneous after 'match' is altered, instead of waiting on each other. - } - - /** - * For case multiple hashes are equal (however unlikely) check the actual key (using longs) - */ - private static boolean memoryEqualsEntry(final long startAddress, final long[] entry, final long finalBytes, final int amountLong) { - for (int i = 0; i < (amountLong - 1); i++) { - int step = i << 3; // step by 8 bytes - if (UNSAFE.getLong(startAddress + step) != entry[i]) - return false; + private static Unsafe initUnsafe() { + try { + final Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe"); + theUnsafe.setAccessible(true); + return (Unsafe) theUnsafe.get(Unsafe.class); + } + catch (NoSuchFieldException | IllegalAccessException e) { + throw new RuntimeException(e); } - // If all previous 'whole' 8-packed byte-long values are equal - // We still need to check the final bytes that don't fit. - // and we've already calculated them for the hash. - return finalBytes == entry[amountLong - 1]; } }