Dullstar's Java Rant

Started by Dullstar, December 22, 2020, 05:56:09 PM

Previous topic - Next topic

0 Members and 1 Guest are viewing this topic.

Dullstar

Edit Simon: This thread arose from NeoLemmix, SuperLemmini and Lix, Feature Comparison by WillLem.




Quote from: SimonThe Java Runtime is beh as a dependency, can't just ship the game binary and DLLs, always have to tell people to install Java.

This is another topic I could go on a very long rant about. As a user, I hate Java (I've also heard things about the language itself, but I can't really speak for that because the fact that any Java code I'd write would have to be run on the JRE is honestly kind of a dealbreaker for me). I hate Java so much.  And it's not just the fact that you have to download a runtime environment that makes this annoying, because you need to download the Python interpreter to run Python code, and that language isn't nearly as irritating about it.

Quote from: WillLem
For what reason? This strikes me very much as a "hating for the sake of it" kind of thing. Java is fine: the vast majority of applications require you to install something, Java RE is something you only install once and then you can run any Java app natively. And, it's multi-platform compatible. I'd say that's relatively convenient, as computer-stuff goes.

What I will say is that Java runs very slowly on my Mac, for some reason, and that is a bit disappointing, but not annoying. It's a 2012 MBP, so it's probably just not been able to keep up with the updates.

Well, since you asked:

Dullstar's Java Rant

A lot of this is based on some... adventures... with modded Minecraft, so I suppose it's possible that I'm directing blame to the wrong place, but...

The JRE has two major issues (or at least, had - I've had the updater silenced for quite some time since I only use it for Minecraft now). First, it has my least favorite type of updater: the one that simply tells you that an update exists, but you have to do all the work yourself. This would always lead to a lot of fun adventures back in the day related to making sure Minecraft had all the memory it needed: the downloads page would (I seriously hope they've fixed this by now) always want to take you to the 32 bit version, so then you'd have to track down the 64 bit version instead, which of course was not on the same page because of course it wasn't. Okay, so you found the 64 bit version, let's try to install it, aaaaand oh it wants to install some other junk we don't want. Untick those boxes for like some McAffee trial or Ask toolbar or whatever it was they were peddling... okay, finally, it's installed! A few days later - there's a Java update! Click here to start updating. You wanted the 32 bit version, right? No? Well, then, find the 64 bit version again... download... start the installer... untick the boxes...

For comparison, the Python interpreter:
Go to the website, click downloads, it's easy to find the other builds if it doesn't default to the one you wanted, run the installer, and you're done. No wild goose chase to track down the 64 bit version, no extra junk to opt-out of during the installation. You only need to do a bit of manual work with the installation if you want to change some of the settings, but the defaults are all reasonable.

The second issue is that the JRE is terrible at memory management. For many applications, this doesn't matter because the memory usage never gets that high, but that's not the case for every application out there. Instead, you must tell it how much memory it should allocate ahead of time. Now sure, there's defaults, but they're not suitable for all applications, and when you have to mess with them, that's when things get pretty nasty. Don't allocate enough, and the program will keep pausing to run the garbage collector. You can give it a bit more breathing room by expanding the amount of memory it has to work with, but be careful if you're running other applications in the background! Allocate too much and the JRE will crash when it needs to use that memory - it can't cleanly handle the system running out of memory. Granted, many applications can't - but Java is garbage collected, so if you run out of memory, there is something it should be able to do: run the garbage collector to free all the junk. So if you've got an application that requires a lot of memory, such as a modded version of Minecraft, you'd better make sure there's no memory hogs running in the background like web browsers can sometimes. Being able to limit the memory usage of an application that's holding onto unneeded stuff in memory simply for performance isn't a terrible idea - that'd be great for making my web browser play nicer with other applications since it likes to hold some stuff in memory to speed up loads and overtime can lead to the browser consuming like half of the available memory even though it doesn't actually need that memory to function. But if you're running something like a game, you probably want the computer to give the game everything it's got (well, if it would benefit from it anyway), and this is where the weaknesses of Java's memory management show through the most. That's because the maximum memory allocation size isn't just a cap: it's also a promise to the JRE that it will receive up to that much memory if it requests it. This means I can't give it free reign to use as much memory as it needs, because I have to choose a maximum memory allocation such that I expect it to always be available. And of course it doesn't allocate this right away (I mean, it really shouldn't, but if it's going to behave the way it does at least this would make it a start-up error instead of a runtime error) - it waits until the application needs that memory, but it also adjusts the behavior of the garbage collection based on this, so you'll have a lot of situations where it decides to allocate more memory instead of running the garbage collector. And then if that memory isn't available, the program crashes! Frustrating!

WillLem

Quote from: Dullstar on December 22, 2020, 05:56:09 PM
First, it has my least favorite type of updater: the one that simply tells you that an update exists, but you have to do all the work yourself

I'm not sure I've experienced that with Java; it always at least provides a clickable link for updating. And, I'd rather be given the choice not to update, since updating Java can cause issues if the apps aren't being maintained.

Quote from: Dullstar on December 22, 2020, 05:56:09 PM
Okay, so you found the 64 bit version, let's try to install it, aaaaand oh it wants to install some other junk we don't want. Untick those boxes for like some McAffee trial or Ask toolbar or whatever it was they were peddling

It definitely doesn't do this anymore. Were you getting your download directly from Oracle or from somewhere like Softonic or Cnet?

Nowadays, the Java SE downloads page is relatively simple, and it provides a list of every possible version you might need. I seem to remember it being less straightforward in the past, though... but it can't have been too much more difficult, because I've been using it! :P

The thing about memory is not something I've ever experienced either, but then I've never run any memory-intensive Java apps. In fact, SuperLemmini is currently the only Java app I use...

Dullstar

It was downloaded from Oracle directly. As far as tracking down the 64 bit version goes, if you're not using anything memory intensive, then the whole 32 vs. 64 bit distinction doesn't really matter unless you're on Mac (because apparently Apple is dropping/has dropped 32 bit support?), or you're on a really dated piece of hardware that doesn't support 64 bit. At least based on the behavior of NeoLemmix and Lix, I doubt SuperLemmini needs enough resources for those issues to come up.

Simon

#3
Quality rant, thanks a lot.

The Oracle JRE is the standard one, but it is not open-source; its standard gratis license does not allow to run applications for commerce/non-privately.

Speed. I don't know how long the Oracle JRE takes to start; classically, JREs took ages start because they have to load lots of bloat. Once it runs, though, the Oracle JRE is fast; they invested lots of research into fast JIT. The managed code will run as fast as native code, maybe even faster depending on what exactly it does.

For the very few Java apps that I use, I've always used OpenJDK, but I haven't run anything performance-heavy. I should pay attention to OpenJDK's start times the next time I reboot the machine, i.e., in a few weeks. >_>

In my day job, there is a Java rewrite of the crusty C/C++ project underway. Both run semi-embedded: The software gets autostarted after Linux boots, and little else runs on that target machine. Allegedly, the Java application needs over a minutes more to start compared to the native application. Don't know if we can chalk 100 % of that up to the JVM, though, but probably for a sizeable chunk of the time, yes.

Quote from: WillLemJava runs very slowly on my Mac, for some reason, and that is a bit disappointing, but not annoying.

Stuff on computers should run blazingly fast. Anything slow is a huge problem. Latency is a huge problem. People should not be conditioned to wait on slow software/hardware, or to consider slowness merely disappointing. Stuff must be blazing fast!

If programs are slow, I will forget what I want to do, I will start other things in the meantime, and will not use those programs in the best possible way. E.g., I will consider workarounds to avoid the slow features. The time-limiting factor should be my thinking, or my typing speed.

This Torvalds quote from the 2007 git talk is spot-on:

Quote from: Torvalds, 2007People seem to think that performance is about doing the same thing, just doing it faster, and that is not true. That is not what performance is all about.

If you can do something really fast, really well, people will start using it differently. One of the things I wanted to make sure is that merges go really really quickly because I want people to merge often and merge early, because as it turns out it becomes easier to merge. [if you merge often]

GUI on Java: The idea that it looks the same on each platform means that it looks awkward on each platform. I believe it's possible to have native widgets instead of the Einheitsbrei, but few designers pick the native widgets for their GUI application. The non-native Java GUIs feel slow, lots of latency, not snappy.

Memory management. One benefit of managed code (i.e., not native code) is that the JRE can force all allocations to go on its managed heap, have fine control during garbage collection (GC) and can implement fast GC algorithms, search://generational gc/.

For games, it's usually okay if they crash when out of memory. Thus, it's sad if the JVM is really so terrible at handling OS memory.




Rambling on memory management, side-tracking, not necessarily a discussion of merits of the Java ecosystem.

Compare the managed code with natively compiled D where we still have a GC, but now we have pointers into native memory: The GC will halt the program, then must scan the entire of the address space of the program, free unused memory, and then resume execution. We can't have the really smart algorithms, I believe this is because the GC isn't allowed to reallocate ad libitum, to keep the native pointers valid. Still, the worse GC isn't a dealbreaker at all in Lix, and I've seen at least one more D game dev say that you can force the GC to run 60 times per second with no problems on typical games.

You can avoid GC in D, but it's not as comfortable. In general, GC is a performant strategy anyway: You don't have to reference-count everything, that would introduce another indirection in your data structures, and you still don't have to manage manually. The closest manual strategy is arena allocation: You allocate a big chunk of memory in advance before play, serve small chunks of it as necessary, never free during play, and only at end of level, you free the entire arena in one single go.

Fine control during ouf-of-memory. It's not necessarily acceptable even for a game to just crash on out-of-memory. If one wants more control during out-of-memory, e.g., to save the game in some allocation-free way before crashing. In sandbox games or grinding games, this appears really important. But I'm not sure how easy that is with the Java memory model.

I'd want to pre-allocate an arena just for that, to still have this memory guaranteed even when the host machine runs out many hours later. These ideas are at the heart of the Zig programming language, where allocation is always explicit, and libraries are encouraged to accept an allocator from the caller instead of hard-wiring one on their own.

The side effect of this is that programs must always be very explicit about error handling. You can't have the nice functional pipelines someRangeOfObjects.map.filter.fold...

-- Simon

WillLem

Quote from: Simon on December 24, 2020, 11:23:58 AM
Stuff on computers should run blazingly fast. Anything slow is a huge problem. Latency is a huge problem. People should not be conditioned to wait on slow software/hardware, or to consider slowness merely disappointing. Stuff must be blazing fast!

Agreed! I wonder what's slowing down SL on my Mac, then... it runs perfectly on Windows (even my really old Win 7 machine can handle it just perfectly).

Dullstar

Quote from: Simon on December 24, 2020, 11:23:58 AM
Rambling on memory management, side-tracking, not necessarily a discussion of merits of the Java ecosystem.

Compare the managed code with natively compiled D where we still have a GC, but now we have pointers into native memory: The GC will halt the program, then must scan the entire of the address space of the program, free unused memory, and then resume execution. We can't have the really smart algorithms, I believe this is because the GC isn't allowed to reallocate ad libitum, to keep the native pointers valid. Still, the worse GC isn't a dealbreaker at all in Lix, and I've seen at least one more D game dev say that you can force the GC to run 60 times per second with no problems on typical games.

You can avoid GC in D, but it's not as comfortable. In general, GC is a performant strategy anyway: You don't have to reference-count everything, that would introduce another indirection in your data structures, and you still don't have to manage manually. The closest manual strategy is arena allocation: You allocate a big chunk of memory in advance before play, serve small chunks of it as necessary, never free during play, and only at end of level, you free the entire arena in one single go.

I'd like to add that no GC doesn't necessarily mean reference counting, although it certainly can: we don't need to count the references if we can safely assume there will only ever be one reference to the object (e.g. std::unique_ptr in C++). I should probably do some tests at some point to compare the performance of GC vs. C++ smart pointers (both shared_ptr and unique_ptr), both in terms of raw speed as well as memory consumption.




I've messed a little bit with GUIs (not a lot), but I'm definitely starting to understand why a lot of designers choose to use non-native solutions for cross platform such as whatever it is Java does for that, or an approach like Lix's editor where it's basically making a GUI out of a framework intended for games.

Simon

#6
Quote from: Dullstar on December 24, 2020, 05:28:38 PM
don't need to count the references if we can safely assume there will only ever be one reference to the object (e.g. std::unique_ptr in C++).

Right, and it's still okay when one lends the object to a different part of the program, as long as they stop using it before we deallocate. This is very common in games. I'd bet 95 % of game code can be easily written in this style.

Quotetests at some point to compare the performance of GC vs. C++ smart pointers (both shared_ptr and unique_ptr), both in terms of raw speed as well as memory consumption.

Single-owner style still deallocates more often than GC or arena allocation, it deallocates every object by its own as soon as the object goes out of scope. It's usually good enough, especially if most of the deallocation happens after play. Or also, I believe, if the objects lived in a contiguous container (std::vector, std::array), then there will be N explicit destructor calls by the container, but only one deallocation.

If this is still not enough, one can fit custom allocators in the template arguments of STL containers. E.g., you can write your own arena allocator that serves preallocated memory to the container and doesn't free even if the container drops objects. Or even a garbage-collecting allocator. I've written a custom STL allocator only once so far, as my own weekend research to print STL container allocation behavior to standard output, and haven't measured allocation performance.

And there are replacements for the STL altogether, e.g., the Embedded Template Library, whose functions never throw to even avoid that overhead.

Likely, performance is dependent on what kind of game object you're handling. Lemmings-likes might keep 100 to 10,000 objects in memory. Games with flashy graphics or physics simulation will have more, that also vanish more often.

Shared pointers are occasionally handy, e.g., for the caching of older game states, for quick backwards framestepping, where it's not clear when any given state will be dropped from the cache. I wouldn't keep shared pointers for lots of little stuff, for fear of too much overhead. In general, lots of little unimportant stuff can be implemented as small value types, and one can keep them all in one std::vector.

-- Simon