10 ms·
Overhead of Returning Optional Values in Java and Rust
- Traubenfuchs 5y agoUsing null is fine. You probably don‘t need Optional. Using Optional is often „clever“ (=bad) overengineering. Every NullPointerExceptipn my code ever caused was good, because it helped find an error state that had no acceptable default non error state alternative.
- valenterry 5y agoNot sure if satire. But if not: > Every NullPointerExceptipn my code ever caused was good, because it helped find an error state that had no acceptable default non error state alternative. Yeah, and with Optional you would have found it at compile-time.
- hashmash 5y agoOnly if you never have the case where Optional is empty but you expected it not to be. How often is Optional.get or orElseThrow called blindly? How often is boilerplate code written which checks if the optional is empty and then throws "should not happen" exception or just logs a message? Once NullPointerException got "helpful" messages, I don't see that much advantage to Optional, considering that it provides less information out of the box.
- convolvatron 5y agofor me this is one of the bigger festering wounds in programming. error handling doubles the size of the code. error handling is complicated. so we put on our big-dev pants and build all sorts of structures around handling errors to make it more sound/ergonomic. but at the end of the day: o its still very difficult to respond in a semantically meaningful way to errors as code o they still almost always just get dumped into a log or ignored, or just result in a panic o even if we have managed to surface them - they still aren't really that actionable by the end user so we haven't helped the situation much, but some of these answer like catch/throw have really unpleasant consequences - we may have made it worse
- valenterry 5y agoIt's pretty much a solved problem. Just use a language that has better ergonomics for errors. Scala's ZIO is one of the better examples if you are interested to look into an alternative.
- esrauch 5y ago> Only if you never have the case where Optional is empty but you expected it not to be I kind of think that sentence is contrary to the point of Optional, and if you start from that mindset then Optionals do end up no better than nulls. Instead you enforce good hygiene where you unwrap early and pass non-Optionals for anything that you "know" is non-null. If you call a function which returns optional, and you're find yourself thinking "nah, I _know_ this is definietly non-empty" then you're incorrect; the API explicitly is telling you that it definitely can be empty.
- hashmash 5y agoThere are certainly cases where Optional is better than a "this can be null" comment, but consider the possibility that the Map interface was designed to always return Optional. If I'm using a Map that I have complete control over, that I know what's in it (I think), then I'll call Map.get(x).orElseThrow(). This isn't an improvement over a Map.get(x) call that can return null. It's also uglier, and as shown by the Rust benchmark, much slower. I'd be happier with a Map interface that had a "tryGet" and "get" pair, where only the latter threw an exception. No need for Optional.
- valenterry 5y agoIt _is_ an improvement. If you just do "Map.get(x)" and you get null because your thinking was wrong, then the NPE will pop up potentially 10 layers later or an hour later, when the null-value was tried to be used in your program. On the other hand, with "Map.get(x).orElseThrow()" you will have an exception thrown immediately! Even better, you should customize that as "Map.get(x).orElseThrow(NoSuchElementException(x))" so that you know the value that was unexpectedly not in the list. That will make debugging much easier and will potentially fail a test case while a "Map.get(x)" might not cause the test to fail.
- thinkharderdev 5y agoThe advantage is if you are explicitly modelling a value that could be empty. You can encode that into the type system by making git an Option<T> instead of just a T (that could be null). At it's most basic level it can just help reduce the possibility of NullPointerExceptions but the real benefit and reduction of boilerplate comes when you use Option as a monad. For instance, in Scala if you have a something like case class Address(street: String, city: String, state, String) case class User(id: Int, name: String, address: Option[Address]) def filterCAUsers(users: List[User]): List[User] = users.filter(_.address.exists(_.state != "CA"))
- shawnz 5y agoUsing "Optional" doesn't provide any extra compile-time guarantees except that the function could return Optional.empty(). It's already possible to see that it could return null, so there's no extra guarantees there. The only advantage of "Optional" is that it lets you deal with the empty/null case with a nice functional API instead of if statements. It doesn't do anything to solve the reverse problem of guaranteeing that non-Optional functions can't return null. If you just want to provide a hint to the caller at compile time about whether you intend for the possibility of returning null, you could use @Nullable/@NotNull annotations to achieve that with no overhead.
- ubercow13 5y agoYes, in a language like Java where it's retrofitted in years after the decision was already made to allow literally everything to be null. In other languages, that doesn't apply.
- brabel 5y agoIt doesn't apply in other languages either as long as their nulls are "safe", as in Kotlin or TypeScript.
- Zababa 5y agoThe thing is that people usually don't have a lot of code to handle the null case, so having optional where you can easily chain the happy case often offers a better experience. It also allows you to differentiate when people will return null for nothing, or when they will return an empty object. I know that in the codebase I work with, people use both. With Optional, you don't have that problem.
- shawnz 5y agoWhy not just chain the happy case by ignoring nulls, and let any potential NullPointerException bubble up to the point in the code where you would have had an "Optional.orElse" or whatever? Isn't that often just as easy? If you want it to be explicit then you could just add "@throws NullPointerException" to your javadoc. And of course having Optional in your type signature doesn't guarantee you'll never return empty objects (or even null).
- setr 5y ago> Every NullPointerExceptipn my code ever caused was good, because it helped find an error state that had no acceptable default non error state alternative. Isn’t that the point of Optional? It just moves that identification of those error states to compile-time rather than at runtime.
- xxs 5y agoMy takes is that Option is (next to) useless - the most common pattern is ...orElseNull() - yikes. The only sane approach is removing all nulls as early as they come and declaring/initializing all member variable in the c-tors.
- setr 5y agoThe problem I think is that if your dependent apis aren’t also consuming Optionals, and relying on type = null || type logic, then you end up constantly translating back and forth between the two endlessly, defeating any ergonomic gains. The ideal state is that anything that isn’t Optional is guaranteed to be non-null. So when you go ahead and remove all nulls as early as possible, you can statically guarantee that remains true forever-more. But if all your libraries just go ahead and drop this knowledge anyways, and when they return an object without meaning non-nullable, then you’re back to where you started. Whereas in an ML, I know every library is already adhering to this, so something being not-optional actually means something. Bolting it into a language ad-hoc, you go from everything can be null to this weird tristate — a type might be nullable, or might not be nullable and if it’s in an optional, it definitely is nullable… so your best bet is just checking for null every time anyways
- plmpsu 5y agoI agree. Add null checking at compile time with something like checkerframework to get the best of both worlds.
- brabel 5y agoExactly. I don't get why people complain about null-pointers in Java when all it takes to get rid of them is enabling static analysis that enforces checking for null at compile time... as that's exactly as safe as using Optional, and as Kotlin has shown, it's also possible to have a nicer interface (e.g. `something?.let { it.isNotNullHere() }`) with bare nulls... without incurring the cost of allocation or of declaring generic types everywhere. Since I started using "safe" nulls with Ceylon, nearly a decade ago, later with Kotlin and TypeScript, I've been firmly in the camp of supporting nulls in the language instead of `Optional` or `Maybe`. Rust should've used them IMO... using `?` for error propagation instead is such a weird choice.
- Zababa 5y ago? is used for Result, and not for Option. For Option, you use .unwrap(). Supporting nulls in the language also means that everything else is going to be second-class, while sum types allows you to do pretty much everything you want.
- kangalioo 5y ago(? works for both Option and Result, as does unwrap(). ? propagates to the caller, while unwrap() initiates a panic and unwinds the stack)
- khuey 5y ago? is used for Option. Just like Result, ? propagates the Err/None to your caller, and unwrap asserts that it's not present.
- deleted 5y ago[deleted]
- Zababa 5y ago> Using null is fine. You probably don‘t need Optional. Using Optional is often „clever“ (=bad) overengineering. Option types are 8 years older than null (ML: 1973, null: 1965). There's nothing clever about it. It's simple to understand, and is an explicit wrapper around an object, while a pointer is an "implicit" pointer around an object. Option<Obj> is clear, you know that it can be something or nothing. Plain Obj is deceiving. > Every NullPointerExceptipn my code ever caused was good, because it helped find an error state that had no acceptable default non error state alternative. That's the point of option types and pattern matching, it can force you to handle both cases.
- alkonaut 5y agoComparing C# (.NET 5) with Rust would perhaps be interesting since at least then it’s languages on the same footing wrt heap allocating Option<> (neither does). But on the other hand then it should mostly be equal for a microbenchmark and that’s not very interesting. Allocating anything like a “Long” or “Optional” on the heap seems like an extremely bad idea. Even if allocation and GC is almost free, the pointer chasing might not be.
- munchler 5y agoI would throw F# into that test as well.
- deleted 5y ago[deleted]
- vips7L 5y agoI’m pretty sure returning Optionals from a method always makes them “escape” and become heap allocated. Hopefully if Valhalla ever lands Optional will be a primitive class. Here’s a fascinating read on the current state of HotSpot escape analysis: https://gist.github.com/JohnTortugo/c2607821202634a6509ec3c321ebf370 https://gist.github.com/JohnTortugo/c2607821202634a6509ec3c3...
- mwcampbell 5y agoAnd this is one reason why I think Kotlin's approach to nullability is better than Scala's Option or Java 8's Optional. Also, the Kotlin solution has better interop with legacy Java APIs.
- bonzini 5y agoDo you have a pointer?
- ptx 5y agohttps://kotlinlang.org/docs/null-safety.html https://kotlinlang.org/docs/null-safety.html
- vips7L 5y agoYeah I’m hoping Java takes the C# approach with a compile option that can enable that type of null safety.
- pjmlp 5y agoThe C# approach will take decades for .NET shops to wrap their minds around it. 2021 was the first year I deploy anything related to .NET Core in production, and it was still .NET Core 3.1. Will this velocity no one is going to their .NET repos and rewrite the libraries with the required annotations to keep the compiler happy. If you want null safety in Java today, just make use of @Nullable, a good IDE with static analysis, and deploy PMD, FindBugs or SonarQube into the CI/CD pipeline.
- 5y ago
- _old_dude_ 5y agoI expect Java to be slower, Optional was introduced as an afterthought not as a native construct like in Rust. Sadly, the two benchmarks are not equivalent, the Rust version loop over ints, the Java version loop over longs. Looping over longs is notoriously slow in Java, because there is no vectorisation and the JIT insert insert a GC checks because looping over longs may take a long of time. Also one of the codes uses Optional<Long> instead of OptionalLong, so the code does a double boxing long -> Long -> OptionalLong.
- comex 5y agoThe Rust version iterates over u64, which is the same size as Java long, though it’s unsigned whereas Java long is signed.
- Gadiguibou 5y ago> Sadly, the two benchmarks are not equivalent, the Rust version loop over ints, the Java version loop over longs. I thought longs were 64 bit signed integers in Java, what's the difference with a u64 in Rust? Is it the signedness that affects performance? > OptionalLong Thanks for the info! I didn't know there were Optional primitive types in Java!
- deleted 5y ago[deleted]
- sushsjsuauahab 5y agoWell, it's not exactly a primitive (long) and instead it is an Object (Long), but I understand what you mean :)
- shawnz 5y agoOptionalLong is an object which optionally contains a value type (lowercase L) long. Optional<Long> is an object Optional which optionally contains an object Long.
- 5y ago
- mastax 5y agoThe most equivalent Rust type to `Long` would be `Option<Box<u64>>`, which is represented as a nullable pointer to a u64. `Box<Option<u64>>` is represented as a non-nullable pointer to a struct containing a u64 number and an aligned-to-u64 discriminant. Does it change the benchmarks? Not if you let the inner function get inlined: https://rust.godbolt.org/z/Ejc5c7sE8 https://rust.godbolt.org/z/Ejc5c7sE8
- nqzero 5y agoJMH intentionally disables many optimizations that hotspot would otherwise perform, so comparing the (optimized) rust performance with the (de-optimized) jmh performance doesn't make any sense
- aw1621107 5y agoReally? That's a surprise to me, considering the supposed purpose of JMH, though to be fair I'm not hugely experienced in it. Do you know what optimizations are disabled? Are they disabled automatically, or only if certain parameters/functions are used (e.g., BlackHole)?
- nqzero 5y agofor the official oracle view, see: https://www.oracle.com/technical-resources/articles/java/architect-benchmarking.html https://www.oracle.com/technical-resources/articles/java/arc... "JMH provides a [snip] foundation for writing and running benchmarks whose results are not erroneous due to unwanted virtual machine optimizations" unsolicited personal opinion: they're almost always erroneous for other reasons i'm not sure which optimizations are currently disabled
- aw1621107 5y agoHmmm. Wonderfully vague. Suppose I'd need to dig into the source to figure it out. To be fair, now that I think about it I suppose some deoptimizations make sense (e.g., preventing replacement of loops). Thanks for pointing me to that article!
- ridiculous_fish 5y agoThis is the first I've seen `num::NonZeroU64` which is nice, intrusively using a zero value to mean none. Is there any analog for a const pointer? I want to point into or just beyond a const array, intrusively using null as a None value. There is `ptr::NonNull` but this always converts to a *mut T, and one is just supposed to "be careful" to not use the mutable features if your pointee isn't actually mutable.
- returningfory2 5y agoAnything of type `&T` has the same sizing property as `num::NonZeroU64`: the size of `Option<&T>` is the same as the size of `&T`. So I think you would just use a plan old Option?
- ridiculous_fish 5y agoThe problem with &T is that you can't make one to just-past-the-end of an array. If you look at the implementation of slice::Iter, it stores a (cursor, end) pointer pair, but there's no way to say "this is a non-null const pointer" so it casts the cursor pointer to *mut. https://doc.rust-lang.org/src/core/slice/iter.rs.html#67 https://doc.rust-lang.org/src/core/slice/iter.rs.html#67
- hn_throwaway_99 5y agoThis is somewhat tangential, but in my opinion implementing Optionals as a "normal" wrapper type, as is done in Java, doesn't really help that much, and can in fact actively hurt, because (a) there is nothing that prevents an Optional variable from actually being null (I've seen a method with an Optional return type that returned null in some cases - obviously a horrible decision, but not prevented by the compiler nonetheless), (b) you don't get compiler support that knows, for example, that if you preface an empty check on the variable that it's then safe to get the value and (c) it makes code super-verbose and hurts readability. You really need first class support for optionals, a la TypeScript and Kotlin, to get a benefit IMO.
- foolfoolz 5y ago- if you don’t use null this isn’t an issue - it’s verbose because handling optional values safely is complex
- Zababa 5y agoHow is "handling optional values safely" complex? Do you have any examples of that?
- hn_throwaway_99 5y ago> if you don’t use null this isn’t an issue I mean, no shit. But the problem is that tons of existing Java APIs do use nulls, so you can't just pretend they don't exist, and importantly a major point of the optional construct is to get compile-time guarantees, and the Optional class doesn't give you that. > it’s verbose because handling optional values safely is complex No, it's not. Kotlin has optionals built into the language, and it's safer and much less verbose.
- tadfisher 5y agoNit: Kotlin doesn't exactly have optionals, it has essentially a type modifier (?) that represents a "nullable" type, i.e. the sum of a type T and the value 'null'. This is indeed safer and terser, therefore strictly more powerful than Java's Optional type, but it begs a question: Why not allow users to create similar higher-kinded types instead of baking this one very specific type kind into the compiler? Their recent decision to axe typeclasses in favor of multiple function receivers, which solves a small subset of problems that HKTs do in Scala and Haskell, make me look for greener pastures whenever I need them.
- Zababa 5y agoI find the focus on Rust vs Java a bit weird. Yes Rust is faster than Java, I sure hope it is. But the interesting thing is that Rust is better at optimizing the overhead of Option. > The most ugly and error-prone solution turned out to be the fastest: primitive types and magic values. That's often the case in "older" languages. ES6 iterators on arrays are usually twice as slow as a simple "for" loop. Which usually means that by targetting ES5 or lower, you get a free optimization by Babel/tsc!
- tialaramex 5y agoThis is a failure of optimisation though. In the NonZeroU64 case Rust can literally do the same trick with magic values ... in the machine code, the magic value is zero, a NonZeroU64 can't be zero and so Rust will squeeze None into that value of the register. The programmer and their colleagues both reading and writing the code don't need to understand how this trick works, don't need to be aware of magic values, and if some day the code needs a full u64 and a NonZeroU64 isn't good enough, they just change it and sure, it might be a little slower because the optimisation wasn't available, but it still works as expected because magic values were just an optimisation not part of the program logic.
- Zababa 5y agoIt is, but Java has been having trouble optimizing things like that since forever. Project Valhalla is still in development. Pointer chasing and boxing is the reality of Java today.
- pornel 5y agoBoth languages are used in high-performance servers (where you know Ruby or Python won’t cut it). HotSpot has a reputation of being close to a Sufficiently Smart Compiler, so it’s interesting to see where its limits are, compared to lower-level language constructs.
- Zababa 5y agoMy mental model would be, from lowest to highest peformance, something like Ruby/Python < JavaScript < Java/C# < Go < C++/Rust. From what I understand, Java is a huge improvement over Python, Ruby and even JS, but is still not the same as Rust, C++ or even Go. Still, those are rough indicators, and not absolute truth. Yon can usually go really far before being stopped by the absolute strict limit of a language. For example, here's a post about Discord switching from Go to Rust with great success https://discord.com/blog/why-discord-is-switching-from-go-to-rust https://discord.com/blog/why-discord-is-switching-from-go-to..., and here's another about how CockroachDB optimizes Go https://www.cockroachlabs.com/blog/how-to-optimize-garbage-collection-in-go/ https://www.cockroachlabs.com/blog/how-to-optimize-garbage-c.... In the same vein, there's Shopify AOTing Ruby using Sorbet and LLVM https://sorbet.org/blog/2021/07/30/open-sourcing-sorbet-compiler https://sorbet.org/blog/2021/07/30/open-sourcing-sorbet-comp.... All of that to say that I'm not surprised that Rust has better defaults compared to Java, as Rust is supposed to be faster, and option types are a code Rust language feature, while they are a relatively recent addition to Java.
- spullara 5y agoKind of silly compare this before value types in Vahalla. Obviously Java optionals will be slower than Rust ones as they are just wrapper objects. However, they should at least rerun it using OptionalLong rather than Optional<Long>.
- thinkharderdev 5y agoI think the point is not really to compare Java to Rust but highlight that in Java you pay a significant performance penalty (relative to representing Optional.empty() as null).
- tialaramex 5y agoAnd without the Rust code, you just know people would say see, Optional is too expensive, this is why I use null. So the Rust here shows that actually no, this style doesn't need to come with a performance cost. It's been more of a surprise to me to see people defending null as somehow preferable style in these comments.
- spullara 5y agoI think what they are saying is that it is the preferrable style in Java for now. As soon as Vahalla lands in the VM, then Optional in Java will be closer to Rust.
- WmyEE0UsWAwC2i 5y agoI don't think that's the best way to use Optional in java. In particular the use of isPresent & get kind of defeats the purpose. Should be something along the lines : ... sum = 0 opt = getOptional(n) sum = sum + opt.orElse(0); ... Also maybe use something like OptionalInt (but for long).
- layer8 5y agoOptional was added to Java in anticipation of Valhalla, which still hasn't materialized. So no suprise here at the overhead.
- exabrial 5y agoI would certainly hope Rust is much faster! But Java is a very quick language after it's optimizing native compiler kicks in. Speaking of, one thing that bothers me about nearly every Java benchmark ever, is nobody checks to see what was _actually_ compiled to native code. To really compare Apples and Oranges, was the code being executed in interpreted mode? What about compiling with Graal? These questions are important for any long-lived program (which most Java programs are). Here, the warmup iteration of "2" and limit of "5" test runs doesn't really accomplish anything. Depending on the JDK the default compilation threshold is 1.5k or 10k IIRC.
- Zababa 5y ago> These questions are important for any long-lived program (which most Java programs are). Is this still true in the world of CI/CD? If you're following "best practices" and doing multiple deployments a day, how much will your performance suffer? I don't have any answers to that, though I suspect that it may be one of the reasons why Go is used so much for "cloud stuff".
- pjmlp 5y agoIf you are doing it properly, JIT caches should be used.
- exabrial 5y ago> Long running 1500 or 10k in production for hot methods should only take a few mins in most production code I'd hope. Workload dependent, of course! And, you can always tune the JDK for your workload. An optimizing compiler is a very powerful tool
- esprehn 5y agoThis code should use OptionalLong instead of Optional.of() which doesn't have primitive overloads and caused wrapping of the long into a Long and extra heap indirections. It still probably can't beat Rust, but what's presented is probably not optimal.
- rakingleaves 5y agoMike Ernst wrote about the downsides of Optional in Java some time back https://homes.cs.washington.edu/~mernst/advice/nothing-is-better-than-optional.html https://homes.cs.washington.edu/~mernst/advice/nothing-is-be... with pluggable type checking it is possible to use `null` safely in Java