[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)