How low can you go? Ultra low latency Java in the real world [video]
youtube.com
youtube.com
It's more likely that people will switch to OCaml or Rust than ever go back to unmanaged memory. First secure, then correct, then fast.
Interestingly enough, that's a philosophy that's valuable and applicable in other skill sets as well. Woodworking/machining comes to mind and other trades.
Safety/security first, then precision, then speed once you got the other 2 working.
I spent a fair amount of time actually benchmarking and iterating on code to make java programs perform better in college, because thankfully they considered that a useful skill. What I see in practice in industry is architectural patterns that supposedly support maintainability at a great cost to performance, with the concrete parts being written by people who don't have any idea what the performance characteristics of the code they're writing are and why.
I think Java has a lot of merit, but it also has an "ecosystem" rife with bloated, dogmatic, hyper-abstracted code that was never really given a thought about optimality and requires purchasing big servers with lots of RAM.
As good at it is, and yes, it was and is good in the grand scheme; I'm disappointed that Java still has so much inertia, and that development and adoption of alternatives is so slow going. I feel as if there is a "it's good enough, why change anything" attitude that stems from lack of understanding that there are still big juicy low hanging fruit to be found and incorporated into our tools, to ultimately make software better AND easier to write/maintain. The line about "lots of smart people have been optimizing the JVM for a long time" seems like a go-to defense of what I see as stagnation.
Java has about 10M developers, who do everything from developing banking systems, GMail, Netflix, to people writing avionics, air-trafic and weapons control, and manufacturing control. Even 1% of Java developers is more than many language's entire ecosystems.
> I think Java has a lot of merit, but it also has an "ecosystem" rife with bloated, dogmatic, hyper-abstracted code that was never really given a thought about optimality and requires purchasing big servers with lots of RAM.
Again, it's really hard to generalize with such a diverse ecosystem. 1% of Java developers is still ~100K developers.
> that stems from lack of understanding that there are still big juicy low hanging fruit to be found and incorporated into our tools
I'm not sure there are, but if so -- go ahead and prove it.
> The line about "lots of smart people have been optimizing the JVM for a long time" seems like a go-to defense of what I see as stagnation.
Well, as one of the many people working on OpenJDK, I won't comment on that :)
Strings in switch Statements:
https://docs.oracle.com/javase/7/docs/technotes/guides/langu...
Supporting a switch on static final references would be a good first step.
I did say vtable dispatch was efficient, but I might not want to split my logic across classes. A chain of if-else is a linear scan and not efficient. Switch in its current form is too limited.
It is well known that Java has no value types (there is a JSR). Even for a primitive type, the collection libraries need to box all values in order to abstract the element type. Every HFT user on Wall Street has had to write their own collections library using custom code generation to work around this.
The JVM has complete knowledge of the class hierarchy. If a method has not been replaced in a subclass then no vtable entry has to be generated and the JVM just uses a static call.
If there's anyone in London who is interested in Java, give it a go. The meetups have a variety of topics, covering many skill levels. You can go sit at the back and slink off if you want, or sometimes they have pizza/beer and you can chat afterwards.
Java has a few things going for it in HFT. The obvious pluses are it's mature and memory safe. What's less obvious is that you can make it low-latency. It takes a lot of work, but it's doable, at which point you have all the nice things: mature ecosystem, speed, latency, safety. It takes a lot of work, because Java was always oriented towards server use cases, as in high-throughput, not low-latency. That's changing by the way, there are two new GC engines coming out that are low-latency oriented. Also, there's been third-party JVMs with low-latency guarantees for quite a while.
Of course, what's between the lines is that there isn't any easy answers for HFT people. You either choose mature safety and do gc gymnastics (because everything is throughput oriented), or you choose manual memory management, which is its own gymnastics.
Anyway, that's my take. I welcome input and contradictions.
You have to have good algorithms optimized to the max for this to matter though.
If you have a low-latency trading component written in Java, a common trick is to continuously bombard it with ‘fake’ inputs to keep the desired code paths nice and hot.
The fake inputs should be virtually indistinguishable from real ones that you would normally act on. The more subtle the difference, the better, e.g., just flip the sign on the timestamp field.
You can use that subtle difference to pick whether the order goes out to the real exchange or a fake exchange. The decision needs to avoid actual branching instructions, though, or the JVM will likely optimize out the ‘real’ hot path, and you’ll fall back to interpreted mode when an actionable ‘real’ event comes in. I usually use a branchless selector to index into an array ([0] goes to a real socket, [1] goes to a black hole socket).
You can also use this technique to make sure you can respond to very rare events quickly. For example, you may want to respond to news signals from Bloomberg. Actionable news is rare, so if you want to keep your news parsing/analysis code warmed up and in the cache, it needs to constantly be reacting to warmup data.
I would never try to bolt those kinds of optimizations onto an existing system. It’d be too easy to miss something.
The compiler alleviates this somewhat by putting the hot path right under the branch instruction so that the fetch that grabs the branch also grabs the start of the hot path as part of the same cacheline.
It sounds minimal, but if that fetch is swapped out of L2 cache due to long periods of inactivity, it can take upwards of 100ns, which starts to add up in HFT.
With profile guided optimization it is possible for the compiler to have much better information about branches than the CPU can guess. Java applies profile guided optimization in real time, with C++ it much more complex to apply.
I don't think that is the case for modern (last 8 years or so) Intel processors. For example, I'm under the impression that gcc's __builtin_expect only affects the layout of the generated code. However I'd love to learn something new here; do you have a source or any additional info you could share?
The hints are purely for the compiler. When branch probabilities are available (either via heuristics, annotations or profile data), it will optimize hot paths differently from cold paths. For example it might be more aggressive with inlinig or vice versa optimize for size. Also will attempt to put cold code in separate pages so that it doesn't get pollute the cache. Also non taken braches are marginally "faster" than taken so it is worth, when possible to put hot code in the non taken branch.
I'm not a compiler writer, I'm sure there is more.
Compiler writers are smart people and can layout code to give the CPU the right hints, so long as they know what the CPU does. CPU manufactures want the compilers writers to do this as the compiler is a significant factor in making one CPU faster than the competition in benchmarks.
PGO and JIT also do a lot of other things that are unrelated to this discussion. Some of those things can have a much larger gain than branch prediction.
When Java gets value objects, this sort of work will begin to get a lot easier in Java as well, but there will be a lot of catching-up to do.
Use memory mapped files to "talk" between processes. Use flyweights for the messages. Avoid strings in the messages wherever possible. Use single-threaded processes wherever possible. Isolate cpus. Pin to isolated cpu. Turn off hyperthreading. Use some form of object pooling.
The last point is the only Java/GC language specific thing.
Having used Chronicle I can confirm that you can see single digit microsecond latency (or better), and reduce 99th percentile jitter with significant analysis.
This.
A lot of people seem to be willfully dismissive of this reality when they are fanboying for their favorite languages.
Ask for memory in C++, you'll take a HUGE hit getting it. Ask for memory in Java, and you'll get it lightning fast, but you'll take a huge hit getting rid of it. Either way, you should carefully plan memory usage up front.
Basically, one road leads to death and despair, the other leads to disease and destruction...
pray you choose wisely.
While it is hard to beat existing general purpose allocators in all the scenarios, it is easy for specific use cases.
Firstly, these trading firms probably have tight control over their server architectures. Secondly, once you start using low-level Java idioms in order to optimize your code, you've started approaching the point where differences in performance between JVM implementations across platforms become noticeable.
JIT compilers optimise based on the actual runtime characteristics of the running code, not the best guesses that a static compiler has to make. The effect can be significant. It's no surprise that the Java leaders in Techempower benchmarks match or surpass the C++ ones.
In this case though you would know exactly the target hardware.
The Techempower benchmarks mostly show how good the entrant's web-serving stack is. Java has a couple of decades of multiple skilled, well-established groups competing to build the fastest web stack. C++ has Boost.Beast, which, although its developers are smart, wise, widely respected, sexy, and moderators of a Slack i frequent, can't compete in terms of resources.
There are a few AOT-compiling JVMs out there, and it's not the case that they reliably outperform the JIT-based JVMs like HotSpot.
The significant differences are in Java mandating array bounds checks, runtime checking for null, no undefined behaviour, etc.
Whereas 90% of the time might be spent in 10% of the code image, no 10% chunk of the image can ever be found that contributes to 90% of its bloat.
1% of the image contributes 1% to the bloat, 10% to 10% and so on.
:)
Rust's future is far from certain. I would feel irresponsible using it on a project that wasn't self contained.
What?
Now I'd be happy to see high perf low latency talks about rust, go, ocaml, haskell, sbcl, whatever. I believe limit-cases like these are always very very instructive.
In my gut I would not choose Haskell for my first choice for a low latency language. I would definitely have looked at Rust before Haskell.
But on practice, I don't think you are missing anything.
I always love optim talks whatever the language (granted its not a clusterfk interpreter).
You can pick languages other than C++ or Java, but for a specialized and bespoke hardware/software combination like HFT, you'll be at a disadvantage vs other teams.
In my experience, or at least something I've seen at 3 different firms now, there's also an issue around the kind of people that pick exotic languages in terms of pragmatism. Often times the Scala/Haskell/whatever else side of the codebase is beautiful and slow moving while the hacky C++ side actually works and is agile.
Compared to the typical functional language applications, Rust is different in that it's mainly being battle tested in the kind of Firefox performance issues that are very interesting to an HFT programmer and Rust easily interfaces with C.
I'm not sure if it's still an issue since I don't use Rust daily; but one recommendation I'd make concerning array bounds checking is to do something like electric-fence and allow the compiler to be told to allocate arrays to end at the page boundary such that the hardware interrupts when you go off the edge.
If the compiler restricts such an array to only being indexed with an unsigned integer, then it can be safe but forego bounds checking altogether since the lower bound is always 0 and the upper bound check is hardware accelerated.
Yeah, a lot of those would be faster now, because SIMD was stabilized. That's the source of the discrepancy on n-body, for example. I don't think anyone has bothered to really submit new implementations.
Most bounds checks can be hoisted out of the compiler can’t figure it out on its own via asserts. In general it’s not a huge problem in real-world code.
You made that claim before, and I showed you the Rust n-body program "vector instructions using SIMD" contributed 5 months ago.
https://benchmarksgame-team.pages.debian.net/benchmarksgame/...
Edit: the correct URL is https://benchmarksgame-team.pages.debian.net/benchmarksgame/....
It shows 0.48 seconds, and 2 seconds, but https://benchmarksgame-team.pages.debian.net/benchmarksgame/... shows 13.27. Why isn't that one on the main page?
HN have now corrected the URL, as I asked.
> It shows 0.48 seconds, and 2 seconds
It shows 3 different workloads:
secs N
0.48 500,000
2.01 5,000,000
20.09 50,000,000
> … but … shows 13.27…Which we can see is the time for `n-body #2` at the largest workload:
secs N
0.54 500,000
1.33 5,000,000
13.27 50,000,000
> Why isn't that one on the main page?I don't know which you consider to be the "main page".
We can see that measurements for `n-body #2` are shown on both:
faster/rust.html
and measurements/rust.htmlRewriting some hot path parts of the stack in Verilog is a usual thing, and rewriting some other parts in Coq is the future.
https://www.janestreet.com/tech-talks/ocaml-all-the-way-down...
Java is just someone's C++ program.
> "Pointer arithmetic, placement new, and other low level features make it hard to determine pointers from non-pointers and to move things in memory."
Real applications in a garbage collected language are mixtures of the managed code, plus unmanaged components (like foreign libraries).
Garbage collected C++ can work in much the same way: there is a framework for managed code that uses GC, and then there are unsafe parts that are analogous to foreign code. (Except, the managed code can much more easily and efficiently interact with this code than the typical FFI.)
There is more than one way to integrate garbage collection into C++, in any case.
Not all garbage-collection schemes move things in memory; only copying and/or compacting garbage collectors do.