0

I'm seeing very slow times iterating over a Chronicle Map - in the below example 93ms per iteration over 1M entries on my 2013 MacbookPro. I'm wondering if there's a better way to iterate or something I'm doing wrong or if this is expected? I know Chronicle Map isn't optimized for iterating but this ticket from a few years ago made me expect much faster iteration times. Toy example below:

    public static void main(String[] args) throws Exception {
    int numEntries = 1_000_000;
    int numIterations = 1_000;
    int avgEntrySize = BitUtil.SIZE_OF_LONG + BitUtil.SIZE_OF_INT;
    ChronicleMap<IntValue, ByteBuffer> map = ChronicleMap.of(IntValue.class, ByteBuffer.class)
            .name("test").entries(numEntries).averageValueSize(avgEntrySize)
            .putReturnsNull(true).create();
    IntValue value = Values.newHeapInstance(IntValue.class);
    ByteBuffer buffer = ByteBuffer.allocate(avgEntrySize);
    for (int i = 0; i < numEntries; i++) {
        value.setValue(i);
        buffer.clear();
        buffer.putLong(i);
        buffer.putInt(i);
        buffer.flip();
        map.put(value, buffer);
    }
    System.out.println("Finished insertion");

    for (int i = 0; i < numIterations; i++) {
        map.forEachEntry(entry -> {
            Data<ByteBuffer> data = entry.value();
            ByteBuffer val = data.get();
        });
    }
    System.out.println("Finished priming");
    long start = System.currentTimeMillis();
    for (int i = 0; i < numIterations; i++) {
        map.forEachEntry(entry -> {
            Data<ByteBuffer> data = entry.value();
            ByteBuffer val = data.get();
        });
    }
    System.out.println(
            "Elapsed: " + (System.currentTimeMillis() - start) + " for " + numIterations
                    + " iterations");

}

Output: Finished insertion Finished priming Elapsed: 93327 for 1000 iterations

jlw
  • 1
  • If you need better than O(n) operation you need to have addition data structures to index the data. Brute force iteration of a large map is always going to test your hardware. – Peter Lawrey May 15 '17 at 06:36
  • In the ticket you mention it appear the entries refers to the capacity not the size used and for largely empty maps it could be sped up. – Peter Lawrey May 15 '17 at 06:39

1 Answers1

1

Your results: 93 milliseconds per 1 million keys exactly match the result of benchmark here: http://jetbrains.github.io/xodus/#benchmarks, so it's in the expected ballpark. 93 ms / 1m keys is 93 ns per key, it "very slow" compared to what? Your map contains 16 MB of payload and it's total off-heap size is ~ 30 MB (FYI you can check that by map.offHeapMemoryUsed()), that is much more than the volume of L3 memory in consumer laptops, so iteration speed is bound by the latency of the main memory. Chronicle Map's iteration is mainly not sequential, so memory prefetch doesn't work. I've created an issue about this.

Also several notes about your code:

  • In your case the value size of the map is constant, so you should use constantValueSizeBySample(ByteBuffer.allocate(12)) instead of averageValueSize(). Even if the map value size wasn't constant, it's preferred to use averageValue() instead of averageValueSize(), because you cannot be sure how many bytes serializers use for the values.
  • Your value seems to be a good use case for value interfaces with two fields. Moreover you already use a value interface as the key type - IntValue.
  • Do benchmarks using JMH
leventov
  • 14,760
  • 11
  • 69
  • 98
  • While iteration probably could be sped up especially for largely empty maps, one should always expect brute force iteration over every entry is an O (n) operation at best and expensive. – Peter Lawrey May 15 '17 at 06:35
  • Thanks for your responses! I meant slow compared to the 1.5us for 3m entries; the linked code appears to be using a map of 3m entries rather than 3m capacity so I was surprised the numbers were so far off. I misread the 'Working with an entry within a context' part of the readme - from that I'd expected to be able to read off-heap memory directly rather than copying the entire value, but that appears to only apply to Value interfaces. If I switch to use Value though the speed is still proportional to the size of value, even though it's only doing Test v=data.get() but not accessing any fields. – jlw May 15 '17 at 16:01
  • Stepping through the code I see it call ((Copyable) using).copyFrom(nativeReference); if I'm reading copyFromMethod in Generators.java correctly it appears it actually does copy over the entire value, and when I look at it with jmc I see 45% of the time going to Heap.copyFrom via ValueReader.read by way of initCachedEntryValue. If that is correct would you consider adding or is there already a way to iterate without copying the value to heap? Or please let me know if I am completely off base on all this; very new to chronicle maps and really appreciate the help! – jlw May 15 '17 at 16:01
  • @jlw "1.5us for 3m entries" is 0.0005 ns / entry that is impossible :) "appears to only apply to Value interfaces" -- also you could achieve with your custom serializers, there are public API instruments for that, Data.bytes(), Values types don't use any kind of back-door/private API. – leventov May 15 '17 at 21:57
  • @jlw for safety, When you call data.get() with Value interface, on-heap instance is used and returned. If you want zero-copy, you should use value = Values.newNativeReference(valueInterface); ... data.getUsing(value) -- inside the loop. However zero-copy doesn't mean that the payload bytes are not touched at all (their header areas are touched anyway) -- so for 12-byte value interface you won't probably see significant difference between zero-copy / non-zero-copy. For value interfaces of 100+ bytes you may start to see some difference. – leventov May 15 '17 at 22:01