Live data from Hacker News

The sorry state of Java deserialization

marginalia.nu

41–50 of 54 posts

Re: The sorry state of Java deserialization

#41

Earlier quoted context omitted.

Desktop - that's my complaint... you wrote that the code samples "barely fit on desktop", which is only true because your CSS wastes over 2/3rds of the screen width I have provided to my user agent. (it's fine though - I use Reader Mode on such user-hostile sites)

Oh yeah, now I understand. I'm struggling to find a width for the layout that makes sense for text (where narrow columns are preferable), but also for code snippets (where you want wider columns).

You may be overthinking it - most blogs simply allow the text to be the width of the window... your article is very readable in Firefox's Reader Mode, so for what it's worth, my $0.02 is that'd be fine

Re: The sorry state of Java deserialization

#42
post #15

Earlier quoted context omitted.

> Also, hard to say without source code but there is a high chance even more efficient methods like Protobuf create a lot of Java objects and that kills cache locality I don’t think this can be claimed that easily without more info, generational GCs work pretty much like an arena allocator, with very good cache locality (think of an ArrayList getting filled with objects that are continuously allocated in short order.…

GC pressure is another factor. Even in the trivial ownership case, gc:ing in a GB/s allocation environment comes at a cost.

Adding Guava testlib's GcFinalization.awaitFullGc() before a benchmark run and -XX:+UseParallelGC, I saw the runtimes decrease by 30s in Bench_Fury_Ordinal and 20s in Bench_ObjectOutputStream. Ideally you would run using JMH to avoid jit warmup issues, previous runs, etc from polluting your results.

Re: The sorry state of Java deserialization

#43
post #42

Earlier quoted context omitted.

GC pressure is another factor. Even in the trivial ownership case, gc:ing in a GB/s allocation environment comes at a cost.

Adding Guava testlib's GcFinalization.awaitFullGc() before a benchmark run and -XX:+UseParallelGC, I saw the runtimes decrease by 30s in Bench_Fury_Ordinal and 20s in Bench_ObjectOutputStream. Ideally you would run using JMH to avoid jit warmup issues, previous runs, etc from polluting your results.

Did you dial up to run for 1 billion items?

In general I'm not a big fan of JMH for testing sustained I/O scenarios, as CPU, OS and storage behavior are extremely relevant, and JMH tends to interfere with access patterns.

Re: The sorry state of Java deserialization

#45

I noticed: 1. Small read buffers. No reason to sequentially read and parse gigabytes only 4kb at a time. 2. parseDelimitedFrom created a new CodedInputStream on every message, which has its own internal buffer; that's why you don't see a buffered stream wrapper in the examples. Every iteration of the loop is allocating fresh 4kb byte[]s. 3. The nio protobuf code creates wrappers for the allocated ByteBuffer on every…

If you're writing out protobufs, isn't the recommended approach to use some record-oriented wrapper? Something like TFRecord. Or is parseDelimitedFrom how that's implemented in the first place?

But Google must surely have some optimized Java libraries for this given that they use protobufs everywhere.

Edit: I found https://beam.apache.org/releases/javadoc/current/org/apache/... which given it was developed by google is probably close to what they use. I wonder if the logic from this could be pulled out for use with non-distributed workflows.

Re: The sorry state of Java deserialization

#47
post #42

Earlier quoted context omitted.

Adding Guava testlib's GcFinalization.awaitFullGc() before a benchmark run and -XX:+UseParallelGC, I saw the runtimes decrease by 30s in Bench_Fury_Ordinal and 20s in Bench_ObjectOutputStream. Ideally you would run using JMH to avoid jit warmup issues, previous runs, etc from polluting your results.

Did you dial up to run for 1 billion items? In general I'm not a big fan of JMH for testing sustained I/O scenarios, as CPU, OS and storage behavior are extremely relevant, and JMH tends to interfere with access patterns.

Nope, I used the default in your github repository (10_000_000). From a quick profile it looked like previous benchmark allocations where crossing into later runs, who were then penalized unfairly, so I made those small adjustments.

Re: The sorry state of Java deserialization

#48

Earlier quoted context omitted.

Author here, I'm away from my computer atm, but I can cook up a repo with each test in a few hours when I get home. I designed the tests as a drag race because that mimics my real world usage.

That's nice - I'd encourage you to play around with attaching e.g. JMC [1] to the process to better understand why things are as they are. I tried recreating your DataInputStream + BufferedInputStream (wrote the 1brc data to separate output files, read using your code - I had to guess at ResultObserver implementation though). On my machine it roughly in the same time frame as yours - ~1min. According to Flight Record…

JFR only samples running Java methods.

I would guess at least some of the bottlenecks are in hardware, the operating system or in native code (including the JVM) in this case.

Re: The sorry state of Java deserialization

#49
post #29

Earlier quoted context omitted.

> Making a header with the city string mapped to an integer would dramatically shrink the file and speed up parsing. Indeed, Parquet will do this for you if you let it. I also wonder how good Java is at optimizing closure creation in a loop, as in: k -> new ResultObserver() The vast majority of those closures are created and never called. C++ might optimize this well if calling a template but has basically no chance…

I believe the term for this is "partial escape analysis" (PEA). GraalVM is the SOTA in the JVM world for this. Here's an oldish blog on this: https://chrisseaton.com/truffleruby/seeing-escape-analysis/ I'm sure PEA is better now, but it's not sure if it's moved beyond scalars (int, float, etc.)

Wouldn't the lambda here by always escape? It gets created in the function and passed into another one. Looking at the byte code it looks like it always allocates in when its in the interpreter:

    invokedynamic #16,  0             // InvokeDynamic #0:apply:()Ljava/util/function/Function;

Re: The sorry state of Java deserialization

#50
post #49

Earlier quoted context omitted.

I believe the term for this is "partial escape analysis" (PEA). GraalVM is the SOTA in the JVM world for this. Here's an oldish blog on this: https://chrisseaton.com/truffleruby/seeing-escape-analysis/ I'm sure PEA is better now, but it's not sure if it's moved beyond scalars (int, float, etc.)

Wouldn't the lambda here by always escape? It gets created in the function and passed into another one. Looking at the byte code it looks like it always allocates in when its in the interpreter: invokedynamic #16, 0 // InvokeDynamic #0:apply:()Ljava/util/function/Function;

I would expect the relevant optimization to be inlining and/or specialization. An inlined function could allow the JIT detect that the hot path creates and destroys an object without ever using it. Alternatively, if the JIT could specialize the callee for the specific function that is passed in, then the overhead would go away.

(One think I like about less heavily JIT-reliant languages is that it can be more obvious when something will incur a runtime cost.)

Post reply on HN