[HN Gopher] Overhead of Returning Optional Values in Java and Rust
       ___________________________________________________________________
        
       Overhead of Returning Optional Values in Java and Rust
        
       Author : gbrown_
       Score  : 72 points
       Date   : 2021-10-16 13:55 UTC (9 hours ago)
        
 (HTM) web link (pkolaczk.github.io)
 (TXT) w3m dump (pkolaczk.github.io)
        
       | alkonaut wrote:
       | Comparing 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 wrote:
         | I would throw F# into that test as well.
        
       | [deleted]
        
       | Traubenfuchs wrote:
       | Using 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.
        
         | Zababa wrote:
         | > 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.
        
         | valenterry wrote:
         | Not 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.
        
           | shawnz wrote:
           | Using "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 wrote:
             | Yes, 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 wrote:
               | It doesn't apply in other languages either as long as
               | their nulls are "safe", as in Kotlin or TypeScript.
        
             | Zababa wrote:
             | The 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 wrote:
               | Why 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).
        
           | hashmash wrote:
           | Only 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.
        
             | esrauch wrote:
             | > 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 wrote:
               | There 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.
        
             | convolvatron wrote:
             | for 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
        
             | thinkharderdev wrote:
             | The 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"))
        
         | setr wrote:
         | > 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 wrote:
           | My 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.
        
         | plmpsu wrote:
         | I agree. Add null checking at compile time with something like
         | checkerframework to get the best of both worlds.
        
           | brabel wrote:
           | Exactly. 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 wrote:
             | ? 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.
        
               | khuey wrote:
               | ? is used for Option. Just like Result, ? propagates the
               | Err/None to your caller, and unwrap asserts that it's not
               | present.
        
               | brabel wrote:
               | Looks like you have much to learn about Rust. Option can
               | also use `?`
               | 
               | Example (from [1]):                   // Assume
               | // fn halves_if_even(i: i32) -> Option<i32>
               | fn do_the_thing(i: i32) -> Option<i32> {             let
               | i = halves_if_even(i)?;                  // use `i`
               | }
               | 
               | Besides, the `?` is syntax sugar for the `try!` macro as
               | explained in the `try!` macro docs [2], and has nothing
               | to do with sum types, at least not directly.
               | 
               | Ceylon would be a closer case where nullable types are
               | represented as sum types and not a special-case in the
               | language (`String | Null` is the same as `String?`)...
               | but Ceylon, as Rust, special-cased the `?` operator for
               | "something"... in Ceylon, they are used for null-checks,
               | and in Rust, for propagating errors via the `try!` macro,
               | which is what my previous comment was about.
               | 
               | [1] https://stackoverflow.com/questions/42917566/what-is-
               | this-qu...
               | 
               | [2] https://doc.rust-lang.org/std/macro.try.html
        
               | Zababa wrote:
               | You're right, I'm not sure how I thought that it worked
               | only with Result. Thanks for the correction.
               | 
               | > Ceylon would be a closer case where nullable types are
               | represented as sum types and not a special-case in the
               | language (`String | Null` is the same as `String?`).
               | 
               | Aren't those union types? My understanding is that they
               | are different from sum types, as you usually have to
               | declare the sum types beforehand, and sum types are
               | collections of values, while union types are collections
               | of types, and are often declared "on the spot". At least
               | that's how they work in Typescript, I don't know much
               | about Ceylon.
               | 
               | To go back to option types, people usually like them
               | because there are well-known patterns to program with
               | them (like map). Of course you can get the same safety
               | with static flow analysis (Typescript does this), but I
               | think part of it is ergonomics, influenced by functional
               | programming. Though if there is an overhead like in Java,
               | I'm not sure about their value.
        
               | kangalioo wrote:
               | (? works for both Option and Result, as does unwrap(). ?
               | propagates to the caller, while unwrap() initiates a
               | panic and unwinds the stack)
        
             | pkolaczk wrote:
             | With Kotlin based approach to nulls you can't represent
             | Option<Option<T>> values.
        
               | brabel wrote:
               | And that's a good thing, no? I've never met a good use
               | cases for double-wrapping values like this.
        
               | Zababa wrote:
               | I'm not an expert on Kotlin but this reminds me of JS
               | promises. The issue was that by not supporting
               | Promise<Promise<T>>, promises were not monadic. It look
               | like the same issue in Kotlin.
        
               | nemetroid wrote:
               | I thought this was a good example:
               | https://news.ycombinator.com/item?id=28746293
        
       | hn_throwaway_99 wrote:
       | This 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.
        
         | herbstein wrote:
         | > This 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
         | 
         | The great thing about Rust is that `Option` _is_ a normal,
         | generic type. You could very easily implement your own
         | equivalent type.
        
           | mqus wrote:
           | Not quite. The compiler does have the knowledge that it can
           | use \0 for representing None in Option<Pointer> types. I'm
           | not sure it can infer that for home-grown Option enums.
           | 
           | So it can optimize the memory overhead of Option<> away
           | completely.
        
             | umanwizard wrote:
             | That is called "niche optimization" and the compiler will
             | in fact still do it for a hand-rolled Option type.
        
               | herbstein wrote:
               | For anyone not sure, here is the proof.
               | 
               | https://godbolt.org/z/4zhWM9hTP
        
         | imoverclocked wrote:
         | First class support for nullable is more basic and needed.
         | 
         | Java On its own it doesn't support this however there are ways
         | of enforcing this in a Java codebase. Eg: Errorprone and
         | NullAway comes to mind here.
        
         | foolfoolz wrote:
         | - if you don't use null this isn't an issue
         | 
         | - it's verbose because handling optional values safely is
         | complex
        
           | Zababa wrote:
           | How is "handling optional values safely" complex? Do you have
           | any examples of that?
        
           | hn_throwaway_99 wrote:
           | > 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.
        
         | Zababa wrote:
         | It works very well in ML-like languages. The problem is when
         | you have a language where everything can be null. TS and Kotlin
         | need first class support for optional because the underlying
         | language has first class support for null.
        
         | heurisko wrote:
         | Java Optionals are useful in Streams.
         | 
         | I've found I agree with the principle that Optionals only make
         | sense for return values (and if the method returns null, that
         | should be treated as a bug).
         | 
         | I've seen Optionals used for member variables, and they might
         | as well just be null. (And are highlighted as such by IDEs like
         | IntelliJ).
        
         | hashmash wrote:
         | > nothing that prevents an Optional variable from actually
         | being null
         | 
         | That's why you're supposed to use Optional<Optional<X>>. ;-)
        
         | Jach wrote:
         | My experience is they helped a lot. a) basically never happened
         | (I have a vague recollection of catching one in code review,
         | but usually my reviews would instead be "instead of returning
         | null here, use Optional please" or figuring out a way to avoid
         | nulls entirely (e.g. empty list when returning collections)) b)
         | I think both Eclipse and IntelliJ will issue a warning if you
         | perform an Optional.get() without an isPresent() check c) it
         | seems less verbose and more readable than null checks,
         | especially if you have a map chain/are using Streams. It could
         | be even nicer, like what Kotlin has, but it's an improvement.
        
       | vips7L wrote:
       | I'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/c2607821202634a6509ec3c3...
        
         | comex wrote:
         | That post says that escape analysis is "heavily dependent on
         | methods pertaining to candidate objects being inlined",
         | implying that the check for whether something is returned from
         | a method happens after inlining. The post then complains that
         | the inliner's decision of whether to inline isn't informed by
         | whether doing so would be beneficial for escape analysis. But
         | that shouldn't matter for this particular microbenchmark where,
         | according to the post, the function returning the optional does
         | get inlined.
        
         | mwcampbell wrote:
         | And 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.
        
           | vips7L wrote:
           | Yeah I'm hoping Java takes the C# approach with a compile
           | option that can enable that type of null safety.
        
           | bonzini wrote:
           | Do you have a pointer?
        
             | ptx wrote:
             | https://kotlinlang.org/docs/null-safety.html
        
           | vbezhenar wrote:
           | Java Optional is not a general replacement for nulls
           | (although people might use it this way). It's a way to
           | replace nulls in method return value, when those nulls
           | indicate missing value. It's a subtle philosophical
           | distinction, but I think that it's worth to remember it.
           | 
           | If you want to improve nulls handling in Java, you should use
           | @NotNull annotation and corresponding linters.
           | 
           | https://stackoverflow.com/a/26328555/315129
        
       | Zababa wrote:
       | I 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 wrote:
         | This 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 wrote:
           | It 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.
        
       | mastax wrote:
       | The 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
        
       | nqzero wrote:
       | JMH 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 wrote:
         | Really? 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)?
        
       | WmyEE0UsWAwC2i wrote:
       | I 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).
        
       | spullara wrote:
       | Kind 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 wrote:
         | I 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 wrote:
           | And 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.
        
       | _old_dude_ wrote:
       | I 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 wrote:
         | The Rust version iterates over u64, which is the same size as
         | Java long, though it's unsigned whereas Java long is signed.
        
         | Gadiguibou wrote:
         | > 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!
        
           | sushsjsuauahab wrote:
           | Well, it's not exactly a primitive (long) and instead it is
           | an Object (Long), but I understand what you mean :)
        
           | [deleted]
        
         | tialaramex wrote:
         | Rust doesn't have types named 'int' and 'long' out of the box.
         | It does have types named e.g. i32, i64, u32 and u64.
         | 
         | Because the function get_int() takes a u64, Rust will conclude
         | that the variable i must be a u64 so as to fit in that
         | parameter.
         | 
         | It seems to me that "Java is bad at 64-bit integers" is also
         | useful information but not pertinent to this benchmark, unless
         | you assume it's trying to compare Java to Rust, at which of
         | course Java will be far slower - but I don't think that's the
         | point of the benchmark at all.
        
       | ridiculous_fish wrote:
       | This 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 wrote:
         | Anything 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 wrote:
           | The 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
        
       ___________________________________________________________________
       (page generated 2021-10-16 23:01 UTC)