7 ms·
Identity hashcodes and monitors are perennially tricky to implement with low space overhead and I think in hindsight that putting them at the base of the object
by titzer 4y ago
Identity hashcodes and monitors are perennially tricky to implement with low space overhead and I think in hindsight that putting them at the base of the object model (i.e. every object has them) was a mistake.
WebAssembly GC objects do not have identity hash codes nor monitors to strive for the lowest space overhead in the base object model.
- Someone 4y ago> Identity hashcodes and monitors are perennially tricky to implement with low space overhead and I think in hindsight that putting them at the base of the object model (i.e. every object has them) was a mistake. I can understand the hashcode choice, but I never understood why they chose to add the ability to lock on any object. IMO it still is a bad choice now on server machines, and it certainly was in a language designed for embedded devices in the early 1990s. Does anybody know what they were thinking? Were they afraid of having to support parallel class hierarchies with a LockableFoo alongside Foo for every class? If so, why? Or did they think most programs would have very few objects, and mostly use reference types?
- paulddraper 4y ago> Does anybody know what they were thinking? Ergonomics. (Plus an inherent assumption of mutable data.) You can write `synchronized (this)` or `synchronized` class method, etc. and it just works.
- derefr 4y agoIf I were the Java1.0 authors, I probably would have: - made a "monitor slot" its own primitive type; - made two overloads of `synchronized` — synchronized(MonitorSlot), and synchronized(Object); where for synchronized(Object), the compiler expects to find a MonitorSlot-typed field with a special system name (e.g. "__jvmMonitorSlot") on the passed-in Object - taken the presence of `synchronized (this)` in body code of a class to implicitly define such a field on the class, meaning you can "just write" `synchronized (this)`, since it becomes `synchronized (this.__jvmMonitorSlot)` and also triggers the creation of the __jvmMonitorSlot field - explicitly define the __jvmMonitorSlot field on Class and other low-object-count system types The only change from today would be that you can't point at some arbitrary Object you didn't define the class for yourself, and say "synchronize on that." Which... why do you want to be doing that again?
- paulddraper 4y agoYes with sufficient complexity, it's possible to achieve transparently the same result.
- titzer 4y agoFor me, the main question is where this complexity lives. To minimize the places where sharp knives need to be used, I would prefer to move this up into a language runtime and have the VM/engine underneath focus on implementing a simpler object model that has the mechanisms to pull off these tricks without resorting to unsafe tricks.
- ShroudedNight 4y agoAt that point, why not just make types you want lock functionality for be declared as 'synchronized' in their class definition: synchronized class Foo { //... }
- derefr 4y agoBecause, for the things that do care about synchronization, they might want multiple explicit MonitorSlot members. It makes more sense to just be able to synchronize on MonitorSlots directly, and then decide where they go. The only reason I added the `synchronized (this)` allowance was because the parent said that they think that that's "good ergonomics" — and presumably, the Java1.0 authors also thought that — and I was trying to suggest an alternative that would preserve what they consider "good ergonomics." But personally, if I was the sole dictator of Java1.0 language design with nobody else to please, I would just have `synchronize(MonitorSlot)` + explicitly-defined MonitorSlot members (that, if you declare one, must always be declared final, and cannot be assigned to in the constructor), and that's it. Just refer to them by name when you need one.
- usrusr 4y agoYou underestimate the weight of that little "(Plus an inherent assumption of mutable data.)" side note in pauldraper's post: that one is really huge and the concise form of his post doesn't really do justice to how clearly it answers the entire sub-thread. Java was driven by the admission that multithreading is hard, and the hybris that this hard problem would go away once and for all if only you added enough OOP state hiding and locks. All those strategies we now use to allow parallelism were either unknown back then, or unfashionable, because they were in conflict with that OOP dream world of "objects talking to each other". The ease of just writing synchronized everywhere and declaring done on the topic of multithreading was as much a selling point of early Java as memory safety.
- Someone 4y agoYes, but the number of things you want to synchronize on in real-life code is limited, and, certainly at the time, memory was scarce. I think only allowing ‘synchronized’ on containers and allowing programmers to opt-in on them would have been the better choice. > Plus an inherent assumption of mutable data. Yet, Strings and the boxed value types (Integer, Long, etc.) are immutable, and you can synchronize on them (or does this matter less for those because of the granularity of the memory allocator?)
- paulddraper 4y ago> Strings and the boxed value types (Integer, Long, etc.) are immutable, and you can synchronize on them Side note: That is a very bad idea, because their object identity is iffy.
- deleted 4y ago[deleted]
- mike_hearn 4y agoAt the time, there was a widespread assumption that massively parallel computing was the future and thus that any serious language had to try and integrate concurrency as a core feature. It wasn't just Java. For many years THE argument for functional languages was you'd better learn them because these languages will soon be automatically parallelized, and that's the only way you'll be able to master machines with thousands of cores. With hindsight we know it didn't work out like that. We got multi-core CPUs but not that many cores. Most code is still single threaded. We got SIMD but very little code uses it. We got GPUs with many cores, but they are programmed with imperative fairly boring C-like languages that don't use locking or message passing or anything else, they're data parallel pure functions. But at the time, people didn't know that. You can see how in that environment of uncertainty "everything will be massively multi-threaded so let's give everything a lock" might have made sense
- paulddraper 4y ago> We got SIMD but very little code uses it. Because programming for SIMD is wildly different than MIMD. (And MIMD is what `synchronized` is for.) The theory was that there would be more machines like AMD Thread Ripper.
- pron 4y agoNeither will Java assuming the next stage of this project goes as planned. The overhead for them will only be allocated on demand for those objects that need them.
- titzer 4y agoI get that monitors are inflated dynamically, but from my reading of the linked JEP, Compressed Object Headers have something like 24 bits allocated for the hashcode in the 64 bit word?
- pron 4y agoRight, but that's the first phase. In the next, I believe the goal is to attempt to reduce the header to 32 bits, and then the header overhead (for all objects) for both monitors and hashcodes will be ~3 bits, basically just to indicate whether or not they're used. For objects where they're used, they will be stored outside the header.
- titzer 4y agoThat's pretty cool. These things are super tricky and are hard to get into a high-performance production system, so I respect the journey :-) For Wasm GC, I think we need programmable metaobjects to be able to combine language-level metadata with the engine-level metadata. I have only a prototype in my head for Virgil and plan to explore this in Wizard soon.
- nicktelford 4y agoTowards the bottom of the JEP, they mention that the ultimate goal is 32 bit object headers, which would necessitate object monitors be tracked on-demand in a side table. That's what the parent was getting at.
- derefr 4y agoGiven https://shipilev.net/jvm/anatomy-quarks/26-identity-hash-code/ https://shipilev.net/jvm/anatomy-quarks/26-identity-hash-cod...: > For identity hash code, there is no guarantee there are fields to compute the hash code from, and even if we have some, then it is unknown how stable those fields actually are. Consider java.lang.Object that does not have fields: what’s its hash code? Two allocated Object-s are pretty much the mirrors of each other: they have the same metadata, they have the same (that is, empty) contents. The only distinct thing about them is their allocated address, but even then there are two troubles. First, addresses have very low entropy, especially coming from a bump-ptr allocator like most Java GCs employ, so it is not well distributed. Second, GC moves the objects, so address is not idempotent. Returning a constant value is a no-go from performance standpoint. How else could identity hashcodes be implemented? Is it just impossible to put a WebAssembly GC base-object as a key into a map?
- titzer 4y agoThe idea is that if your language has an identity hash code for every object, the language runtime can just add a field in the Wasm struct for it. There's nothing special about that field; it could be an 8-bit, 16-bit, 32-bit field, etc. For the lock inflation logic and so on, you can make a "tagged pointer" in Wasm GC by using the i31ref type, so you could do something like have only the identity hash code by default, but "inflate" to a (boxed) indirection with an additional monitor dynamically. But the Wasm engine just treats it all as regular fields. The overall idea is the Wasm GC gives you the mechanisms by which you can implement your specific language's needs, hopefully as efficiently as a native VM could.
- moonchild 4y agoOne proposed strategy (https://wiki.openjdk.org/display/lilliput#Main-Hashcode https://wiki.openjdk.org/display/lilliput#Main-Hashcode) for dealing with hashes is to compute them lazily, taking advantage of the fact that most objects are never hashed: initially use the object's address, but also set a flag on the object when it is first hashed; when the gc next moves the object, it adds an extra field in which the original address is cached. Another strategy, used by sbcl, is to rehash identity hash tables after each gc cycle. How could I implement either of these with wasm?
- mastax 4y agoI wondered why .NET let you lock any object, it seems like a strange feature. I figured they just had a spare bit in a bitset somewhere. Should've known it was copied from Java.
- kevingadd 4y agoIn practice (IIRC, I'd have to check again to be 100% sure) there's a little 'extra garbage' (my name, the real name is something else) pointer inside .NET object headers, and if your object has an identity hashcode or has been used with locking, the extra garbage pointer points to a separate heap allocation containing the hashcode and the locking info. That way you're not paying for it for every object. I think in some cases there are optimizations to be able to store the hashcode inline (tagging, etc). So it ends up being similar to these 'compact object headers' described here in terms of size but is done by making those optional features more expensive instead of by bitpacking stuff into the object header. Caveat: There are multiple .NET implementations so what I've said may not apply to all of them
- mike_hearn 4y agoAs the JEP explains, the JVM has done both those things for a long time. In particular the object doesn't actually pay the price of a lock unless it's actually locked at some point. It used to be the case that the JVM was even more extreme and you wouldn't pay the price of a lock unless the object was actually contended, this was called biased locking, but the code complexity to implement it was eventually determined to be no longer worth it on modern hardware.
- PaulHoule 4y agoJava is also considering making value objects that don't have that overhead: https://openjdk.org/jeps/8277163 https://openjdk.org/jeps/8277163 .NET has something like that already, but it would make a huge difference for things like Optional or if you want to make a type for, say, complex numbers where there is just 64-bits of data (for FP32) and even a 64-bit object header would be 100% overhead. Value objects could be possibly allocated on the stack and completely bypass the garbage collector in some cases. Note that with that overhead, Java is an environment in which you can write multithreaded applications with good reliability and scaling, something that WebAssembly definitely isn't.
- bcrosby95 4y agoI've been lightly following some of their work here. From the outside, it seems like a lot of this has been born from optimizing the JVM over the past few decades, running into fundamental problems with how dynamic the JVM can be, then targeting changes for making that dynamicism opt-in, either through JVM flags or new features. I do think some of it is kinda interesting: some people might say "if you never do X, we can guarantee Y". But it's nice, as a programmer, to be told "if you use feature A, you will never do X, which means we can guarantee Y". It's a lot more comforting that I can't slip up and accidentally do X.
- titzer 4y ago> It's a lot more comforting that I can't slip up and accidentally do X. Indeed, this is why value semantics (i.e. structural equality) for language constructs like ADTs is so wonderful. Because a program can never observe "identity", which is an implementation detail, the implementation doesn't have to use objects at all underneath. That opens a whole host of value representation options that aren't otherwise available.
- kccqzy 4y agoMonitors are especially strange when I learned Java. I mean, why would you put a synchronization feature into every object? It's not necessary in the vast majority of the time. Maybe the programming style at the time had something to do with it. Maybe they thought every class needs to be thread safe and mutable.
- layer8 4y agoThis was over 30 years ago when there was still a lack of awareness that locks don't compose [0]. Look at the early JRE classes like Vector, Hashtable, and StringBuffer, whose operations are synchronized. The idea was that since you'd potentially want to synchronize access to any mutable object (because you'd want to be able to share any object between threads), and most objects are mutable, that the most convenient solution would be for every object to have that functionality built in. [0] https://en.wikipedia.org/wiki/Lock_(computer_science)#Lack_of_composability https://en.wikipedia.org/wiki/Lock_(computer_science)#Lack_o...
- vbezhenar 4y agoYeah, I asked why there're no Hashable & Equatable interfaces instead of putting things into Object which would make more sense to me. People responded that's just how things should be. Apparently not. IMO object identity is almost always not needed and a sign of bad design. You either should write proper hash code, or you should not use data structures which use hash code.
- cesarb 4y ago> You either should write proper hash code, or you should not use data structures which use hash code. There's also the question of which hash code. Do you use a fast but low quality hash function, or a slower but higher quality one? Does your hash function need to be secure against hash collision attacks? Does it have to be deterministic? The correct choice of hash function can depend on the data structure, and the same object might have to be hashed with different hash functions (or different hash function seeds) in the same program.
- mike_hearn 4y agoThe reason is that it'd make HashMap and HashSet a lot less useful. If every type had to opt in, you'd be unable to use many types as keys or set entries for no better reason than the authors didn't bother to implement hash codes or equality. By providing reasonable identity-based versions of these, it increases the utility of the language at a tradeoff in memory usage.
- jzoch 4y agoyeah this is where traits instead of hierarchies become useful - I should be able to implement the Hash interface for an object I do not own and then use that object + trait going forward for HashMap and HashSet. Java doesnt make this very composable
- nusaru 4y agoShould be noted that Rust (one of the most prominent languages with traits) doesn't allow you to implement a trait for an object you do not own. A common workaround is to wrap that object in your own tuple struct and then implement the trait for that struct.
- hinkley 4y agoEven in JDK 1.3 era I recognized that anyone could grab a lock on any object, not just the object itself, so that was a bit of a problem. I started experimenting with member variables called “lock” that were new Object(). It seemed pretty dumb at first but then I discovered lock splitting and it was off to the races. There’s always going to be some object with multiple concerns, especially if one or two functions involve both but others involve one (eg, add/delete versus generate data from the current state). Am I misremembering that there was a time where the JRE lazily added locks to objects? I thought it was part of their long road to lock elision.
- mike_hearn 4y agoIt does. It's called lock inflation. The lock data structure is more than just a handful of bits, so the object header can point to the real lock once allocated. You don't pay a full lock for every object, just the memory needed to determine if it's in use or not.
- pjmlp 4y agoA consequence of following too much Smalltalk's model. A better decision would have been to make them available via interfaces, like CLR ended up doing in .NET 2.0, although they still follow Java/Smalltalk as well, due to its heritage.