Skip to content(if available)orjump to list(if available)

Inheritance was invented as a performance hack (2021)

masfoobar

I think many developers, especially in the range on 1999-2020, has gone through many pitfalls in programming. More specifically.. OOP.

As someone who was blessed/lucky to learn C and Pascal.. with some VB6.. I understood how to write clean code with simple structs and functions. By the time I was old enough to get a job, I realised most (if not all) job adverts required OOP, Design Patterns, etc. I remember getting my first Java book. About 1,000 pages, half of which was about OOP (not Java directly)

I remember my first job. Keeping my mouth shut and respecting the older, more experienced developers. I would write code the way I believed was correct -- proper OOP. Doing what the books tell me. Doing what is "cool" and "popular" is modern programming. Hiding the data you should not see, and wrapping what you should in Methods... all that.

Nobody came to me and offered guidance but I learned that some of my older codebase with Inheritence, Overrides.. while it was "proper" code, would end up a jumbled mess when it required new features. One class that was correctly setup one day needed to be moved about, affecting the class hierarchy of others. It brings me back to thinking of my earlier programming days with C -- and to have things in simples structs and functions is better.

I do not hate on OOP. Afterall, in my workplace, am using C# or Python - and make use of classes and, at times, some inheritence here and there. The difference is not to go all religious in OOP land. I use things sparingly.

At work, I use what the Companies has already laid out. Typically languages that are OOP, with a GC, etc. I have no problem with that. At home or personal projects, I lead more towards C or Odin these days. I use Scheme from time-to-time. I would jump at the opportunity to using Odin in the workplace but I am surrounded by developers who dont share my mindset, and stick to what they are familiar with.

Overall, his Conclusion matches my own. "Personally, for code reuse and extensibility, I prefer composition and modules."

roenxi

I'm not sold the evidence is there to show inheritance is a good idea - it basically says that constructors, data storage and interfaces need to be intertwined. That isn't a very powerful abstraction, because they don't need to be and there isn't an obvious advantage from doing so over picking up the concepts separately as required. And inheritance naturally suggests grouping interfaces into a tree in the way that seems of little value because in practice a tree probably doesn't represent the fundamental truth of things. Weird edge cases like HTTP over non-TCP protocols or rendering without screens start throwing spanners into a tree of assumptions that never needed to be made and pull the truth-in-code away from the truth-in-world.

All that makes a lot of sense if it was introduced as a performance hack rather than a thoughtfully designed concept.

osigurdson

Yeah ya... everyone likes to go on and on about how inheritance is the root of all evil and if you just don't use it, everything will be fine. Sorry, it won't be fine. Your software will still be a mess unless it is small and written three times by the same person who knows what they are doing.

The bottom line is, no one ever really used inheritance that much anyway (other than smart people trying to outsmart themselves). People created AbstractFactoryFactoryBuilders not because they wanted to, but because "books" said to do stuff like and people were just signaling to the tribe.

So now, we are now all signaling to the new tribe that "inheritance is bad" even though we proudly created multiple AFFs in the past. Not very original in my opinion since Go and Rust don't have inheritance. The bottom line is, most people don't have any original opinions at all and are just going with whatever seems to be popular.

josephg

> The bottom line is, no one ever really used inheritance that much anyway

If you think that, you have no idea how much horrible code is out there. Especially in enterprise land, where deadlines are set by people who get paid by the hour. I once worked on a java project which had a method - call a method - call a method - call a method and so on. Usually, the calls were via some abstract interface with a single implementor, making it hard to figure out what was even being executed. But if you kept at it, there were 19 layers before the chain of methods did anything other than call the next one. There was a separate parallel path of methods that also went 19 layers deep for cleaning up. But if you follow it all the way down, it turns out the final method was empty. 19 methods + adjacent interface methods all for a no-op.

> The bottom line is, most people don't have any original opinions at all and are just going with whatever seems to be popular.

Most people go with the crowd. But there's a reason the crowd is moving against inheritance. The reason is that inheritance is almost always a bad idea in practice. And more and more smart people talking about it are slowly moving sentiment.

Bit by bit, we're finally starting to win the fight against people who think pointless abstraction will make their software better. Thank goodness - I've been shouting this stuff from the rooftops for 15+ years at this point.

HdS84

I don't think Inheritance is always bad - sometimes it's a useful tool. But it was definitely overused and composition, interfaces work much better for most problems.

Inheritance really shines when you want to encapsulate behaviour behind a common interface and also provide a standard implementation. I.e: I once wrote a RN app which talked to ~10 vacuum robots. All of these robots behaved mostly the same, but each was different in a unique way. E.g. 9 robots returned to station when the command "STOP" was send, one would just stop in place. Or some robots would rotate 90 degrees when a "LEFT" command was send, others only 30 degrees. We wrote a base class which exposed all needed commands and each robot had an inherited class which overwrote the parts which needed adjustment (e.g. sending left three times so it's also 90 degrees or send "MOVE TO STATION" instead of "STOP").

silisili

I think that's part of the charm of Go, as a language/community.

I've worked with countless people who came from Java, who try to create the same abstractions and factories and layers.

When I chide them, it's like realizing the shackles are off, and they have fun again with the basics. It leads to much more readable, simple code.

This isn't to say Java is bad and Go is good, they're just languages. It's just how they're typically (ab)used in enterprises.

rixed

I believe you just summed up 90% of popular wisdom about software engineering.

With enough patience you will see many fads pass twice like a tide raising and falling. OOP, runtime typing, schema-less databases and TDD are the first to come to mind.

I feel "self-describing" data formats and everything agile are fading already.

Very few ideas stick, but some do: I do not expect GOTO to ever come back, but who knows where vibe coding will lead us :)

kragen

I'm on the fence about inheritance myself; I often regret having used it, and I never regret having not used it. On the other hand, it's awfully expedient. I designed and implemented a programming language called Bicicleta whose only argument-passing mechanism is inheritance, and I'm not sure that was a bad idea.

The object-oriented part of OCaml, by the way, has inheritance that's entirely orthogonal to interfaces, which in OCaml are static types. Languages like Smalltalk and, for the most part, Python don't have interfaces at all.

echelon

Rigid, "family tree"-style inheritance as in classical OOP is pretty much garbage. "A cow is a mammal is an animal" is largely useless for the day to day work we do except in extremely well-planned, large and elaborate ontologies -- something you typically only see in highly structured software like windowing systems. It just isn't useful for the majority of our work.

"Trait/Typeclass"-style compositional inheritance as in Rust and Haskell is sublime. It's similar to Java interfaces in terms of flexibility, and it doesn't enforce hierarchical rules [1]. You can bolt behaviors and their types onto structures at will. This is how OO should be.

I put together a visual argument on another thread on HN a few weeks ago:

https://imgur.com/a/class-inheritance-vs-traits-oop-isnt-bad...

[1] Though if you want rules on bounds and associated types, you can have them.

DaiPlusPlus

> "Trait/Typeclass"-style compositional inheritance as in Rust and Haskell is sublime. It's similar to Java interfaces in terms of flexibility, and it doesn't enforce hierarchical rules.

Yes-and-no.

Interfaces still participate in inheritance hierarchies (`interface Bar extends Foo`), and that's in a way that prohibits removing/subtracting type members (so interfaces are not in any way a substitute for mixins). Composition (of interfaces) can be used instead of `extends`, but then you lose guarantees of reference-identity - oh, and only reference-types can implement interfaces which makes interfaces impractical for scalars and unusable in a zero-heap-alloc program.

Interface-types can only expose virtual members: no public fields - which seems silly to me because a vtable-like mechanism could be used to allow raw pointer access to fields via interfaces, but I digress: so many of these limitations (or unneeded functionality) are consequences of the JVM/CLR's design decisions which won't change in my lifetime.

Rust-style traits are an overall improvement, yes - but (as far as my limited Rust experience tells me) there's no succinct way to tell the compiler to delegate the implementation of a trait to some composed type: I found myself needing to write an unexpectedly large amount of forwarding methods by hand (so I hope that Rust is better than this and that I was just doing Rust the-completely-wrong-way).

Also, oblig: https://boxbase.org/entries/2020/aug/3/case-against-oop/

zozbot234

Rust actually allows one to express "family tree" object inheritance quite cleanly via the generic typestate pattern. It isn't "garbage", it totally has its uses. It is however quite antithetical to modularity: the "inheritance hierarchy" can only really be understood as a unit, and "extensibility" for such a hierarchy is not really well defined. Hence why in practice it mostly gets used in cases where the improved static checking made possible by the "typestate" pattern can be helpful, which has remarkably little to do with "OOP" design as generally understood.

ninetyninenine

> a tree probably doesn't represent the fundamental truth of things

It does. Trees appear in nature all the time. It's the basis of human society, evolution and many things.

Most of programming moves towards practicality rather then fundamental truth. That's why you get languages like golang which are ugly but practical.

codr7

But they're not building trees, that's how inheritance is mostly used today.

After reading this, I'm thinking that intrusive lists is the one use of inheritance in C++ that makes any sense.

josephg

I'd still generally prefer intrusive lists to be done via composition. I've seen plenty of intrusive lists where each item was a member of multiple lists at the same time - which is quite hard to do if you need to inherit from an intrusive list element superclass.

dataflow

> And inheritance naturally suggests grouping interfaces into a tree in the way that seems of little value because in practice a tree probably doesn't represent the fundamental truth of things.

"This doesn't represent the fundamental truth" does not imply "this has little value". Your navigation software likely doesn't account for cars passing each other on the road either -- or probably red lights for that matter -- and yet it's still pretty damn useful. The sweet spot is problem- and model-dependent.

tippytippytango

If you pretend/imagine it was intentional, and insightful, you've created a nerd trap for amateur ontologists. Some of which decide to become professional ontologists and sell books on objected oriented design.

tobr

Your lovely typo there makes me realize how often I’ve had to deal with objection-oriented programming.

ninetyninenine

It is a good idea because it's the most fundamental idea.

You have two objects. A and B. How do you merge the two objects? A + B?

The most straight forward way is inheritance. The idea is fundamental.

The reason why it's not practical has more to do with human nature and the limitations of our capabilities in handling complexity then it has to do with the concept of inheritance itself.

Literally think about it. How else do you merge two structs if not using inheritance?

The idea that inheritance is not fundamental and is wrong in nature is in itself mistaken.

josephg

> Literally think about it. How else do you merge two structs if not using inheritance?

What? Using multiple inheritence? That's one of the worst ideas I've ever seen in all of computer science. You can't just glue two arbitrary classes together and expect their invariants to somehow hold true. Even if they do, what happens when both classes implement a method or field with the same name? Bugs. You get bugs.

I've been programming for 30 years and I've still never seen an example of multiple inheritance that hasn't eventually become a source of regret.

The way to merge two structs is via composition:

    struct C {
        a: A,
        b: B,
    }
If you want to expose methods from A or B, either wrap the methods or make the a or b fields public / protected and let callers call c.a.foo().

Don't take my word for it, here's google's C++ style guide[1]

> Composition is often more appropriate than inheritance.

> Multiple inheritance is especially problematic, because it often imposes a higher performance overhead (in fact, the performance drop from single inheritance to multiple inheritance can often be greater than the performance drop from ordinary to virtual dispatch), and because it risks leading to "diamond" inheritance patterns, which are prone to ambiguity, confusion, and outright bugs.

> Multiple inheritance is permitted, but multiple implementation inheritance is strongly discouraged.

[1] https://google.github.io/styleguide/cppguide.html#Inheritanc...

iExploder

I guess the point was never how to do thing properly, but:

"how to join two struts with least amount of work and thinking so my manager can tick off a box in excel"

in such case inheritance is a nice temporary crutch

ninetyninenine

>What? Using multiple inheritence?

You just threw this in out of nowhere. I didn't mention anything about "multiple" inheritance. Just inheritance which by default people usually mean single inheritance.

That being said multiple inheritance is equivalent to single inheritance of 3 objects. The only problem is because two objects are on the same level it's hard to know which property overrides which. With a single chain of inheritance the parent always overrides the child. But with two parents, we don't know which parent overrides which parent. That's it. But assume there are 3 objects with distinct properties.

   A -> B -> C
would be equivalent to

   A -> C <- B. 
They are isomorphic. Merging distinct objects with distinct properties is commutative which makes inheritance of distinct objects commutative.

   C -> B -> A == A -> B -> C
>I've been programming for 30 years and I've still never seen an example of multiple inheritance that hasn't eventually become a source of regret.

Don't ever tell me that programming for 30 years is a reason for being correct. It's not. In fact you can be doing it for 30 years and be completely and utterly wrong. Then the 30 years of experience is more of a marker of your intelligence.

The point is YOU are NOT understanding WHAT i am saying. Read what I wrote. The problem with inheritance has to do with human capability. We can't handle the complexity that arises from using it extensively.

But fundamentally there's no OTHER SIMPLER way to merge two objects without resorting to complex nesting.

Think about it. You have two classes A and B and both classes have 90% of their properties shared. What is the most fundamental way of minimizing code reuse? Inheritance. That's it.

Say you have two structs. The structs contain redundant properties. HOW do you define one struct in terms of the other? There's no simpler way then inheritance.

>> Composition is often more appropriate than inheritance.

You can use composition but that's literally the same thing but wierder, where instead of identical properties overriding other properties you duplicate the properties via nesting.

So inheritance

   A = {a, b}, C = {a1}, A -> C = {a1, b}
Composition:

   A = {a, b}, C = {a1}, C(A) = {a1, {a, b}}
That's it. It's just two arbitrary rules for merging data.

If you have been programming for 30 years you tell me how to fit this requirement with the most minimal code:

given this:

   A = {a, b, c, d}
I want to create this:

   B = {a, b, c, d, e} 
But I don't want to rewrite a, b, c, d multiple times. What's the best way to define B while reusing code? Inheritance.

Like I said the problem with inheritance is not the concept itself. It is human nature or our incapability of DEALING with the complexity that arises from it. The issue is the coupling is two tight so you make changes in one place it creates an unexpected change in another place. Our brains cannot handle the complexity. The idea itself is fundamental not stupid. It's the human brain that is too stupid to handle the emergent complexity.

Also I don't give two flying shits about google style guides after the fiasco with golang error handling. They could've done a better job.

dang

Discussed at the time:

Inheritance was invented as a performance hack - https://news.ycombinator.com/item?id=26988839 - April 2021 (252 comments)

plus this bit:

Inheritance was invented as a performance hack - https://news.ycombinator.com/item?id=35261638 - March 2023 (1 comment)

rakejake

IMHO Inheritance (especially the C++ flavored inheritance with its access specifiers and myriad rules) has always scared me. It makes a codebase confusing and hard to reason with. I feel the eschewing of inheritance by languages such as Go and Rust is a step in the right direction.

As an aside, I have noticed that the robotics frameworks (ROS and ROS2) heavily rely on inheritance and some co-dependent C++ features like virtual destructors (to call the derived class's destructor through a base class pointer). I was once invited to an interview for a robotics company due to my "C++ experience"and grilled on this pattern of C++ that I was completely unfamiliar with. I seriously considered removing C++ from my resume that day.

cturner

We use the word inheritance to refer to two concepts. There is implementation-inheritance. There is type-inheritance. These ideas are easily confused, which should be cause to have distinct words for them. Yet we don't. (Although Java does, effectively)

impure

Huh, I was always told that inheritance hurt performance as it requires additional address lookups. Thats why many game engines are moving away from it.

I guess it could simplify the GC but modern garbage collectors have come a long way.

kragen

No, inheritance does not require additional address lookups. Single inheritance as discussed here doesn't even require additional address arithmetic; the address of the subclass instance is the same as the address of the superclass instance.

Yes, current GCs are very fast and do not suffer from the problems Simula's GC suffered from. Nevertheless, they do still have an easier time when you embed record A as a field of record B (roughly what inheritance achieves in this case) rather than putting a pointer to record A in record B. Allocation may not be any faster, because in either case the compiler can bump the nursery pointer just once (with a copying collector). Deallocation is maybe slightly faster, because with a copying collector, deallocation cost is sort of proportional to how much space you allocate, and the total size of record B is smaller with record A embedded in it than the total size of record A plus record B with a pointer linking them. (That's one pointer bigger.) But tracing gets much faster when there are no pointers to trace.

You will also notice from this example that it's failing to embed the superclass (or whatever) that requires an additional record lookup. And probably a cache miss, too.

I think the reason many game engines are moving away from inheritance is that they're moving away from OO in general, and more generally the Lisp model of memory as a directed graph of objects linked by pointers, because although inheritance reduces the number of cache misses in OO code, it doesn't reduce them enough.

I've written about this at greater length in http://canonical.org/~kragen/memory-models/, but I never really finished that essay.

josephg

> No, inheritance does not require additional address lookups. Single inheritance as discussed here doesn't even require additional address arithmetic; the address of the subclass instance is the same as the address of the superclass instance.

Yes it does! Inheritance itself is fine, but inheritance almost always means virtual functions - which can have a significant performance cost because of vtable lookups. Using virtual functions also prevents inlining - which can have a big performance cost in critical code.

> Nevertheless, they do still have an easier time when you embed record A as a field of record B (roughly what inheritance achieves in this case) rather than putting a pointer to record A in record B.

Huh? No - if you put A and B in separate allocations, you get worse performance. Both because of pointer chasing (which matters a great deal for performance). And also because you're putting more pressure on the allocator / garbage collector. The best way to combine A and B is via simple composition:

    struct C { a: A, b: B }
In this case, there's a single allocation. (At least in languages with value types - like C, C++, C#, Rust, Swift, Zig, etc). In C++, the bytes in memory are actually identical to the case where B inherits from A. But you don't get any class entanglement, or any of the bugs that come along with that.

> I think the reason many game engines are moving away from inheritance is that they're moving away from OO in general

Games are moving away from OO because C++ style OO is a fundamentally bad way to structure software. Even if it wasn't, struct-of-arrays usually performs better than arrays-of-structs because of how caching works. And modern ECS (entity component systems) can take good advantage of SoA style memory layouts.

The performance gap between CPU cache and memory speed has been steadily growing over the last few decades. This means, relatively speaking, pointers are getting slower and big arrays are getting faster on modern computers.

andyferris

As I understand it, back when Simula and LISP were invented it was generally the case that loads and stores took 1 cycle and there were no CPU caches. These pointer-chasing languages and techniques really weren't technically bad for the computers of the time - it's just that we have a larger relative penalty for randomly accessing our Random Access Memory these days so locallity is important (hence data-oriented design, ECS, etc).

I am kind of amused they _removed_ first-class functions though!

int_19h

Function arguments weren't actually first-class to begin with. In Algol 60 (of which Simula started as a superset), you could pass functions as arguments to other functions, but that's it - it wasn't a proper type so you couldn't return it, shove it into a variable, have an array of functions etc. Basically, it had just enough restrictions that you would never get up in a situation where you could possibly call a function for which the corresponding activation frame (i.e. locals) could be gone. But when Simula added classes and objects, now you could suddenly capture arguments in a way that allows them to outlive the callee.

wbl

Dynamic invocation, not strict inheritance is the issue here. Simply getting functions and fields from a superclass costs nothing if at each callsite the compiler knows enough to say where it is from.

nine_k

But this may only happen when no virtual / overridden methods are involved, no VMT to look up in, no polymorphism at play. This is tanamount to composition, which should be preferred over inheritance anyway.

In this regard, Go and Rust do classes / objects right, Java provides the classical pitfalls, and C++ is the territory where unspeakable horrors can be freely implemented, as usual.

vlovich123

Parent is correct - if the compiler has the information to devirtualize it becomes direct dispatch regardless of the mechanisms involved at the source level. This is also typically true for JITs.

Reason077

Nah. Classic C++/Java style inheritance with vtable dispatch is very fast. Generally no slower than a C-style function call, and actually sometimes faster depending on how the C code is linked, characteristics of the CPU, etc.

nine_k

This assumes that the vtables stay in at least L2 cache, which may be a correct assumption for the few hot-path classes. In this regard, I remember how Facebook's android app once failed to build when the codebase exceeded the limit of 64k classes.

xxs

No, Java does Class hierarchy analysis and has multiple way not to use v-table calls.

Single site (no class found overriding a method) are static and can be inlined directly. Dual call sites use a class check (which is a simple equality), can be inlined, no v-table. 3-5 call sites use inline caches (e.g. the compiler records what class have been used) that are similar and some can be inlined, usually plus a guard check.

Only high polymorphic calls use v-table and in practice is a very rare occasion, even with Java totally embracing inheritance (or polymorphic interfaces)

Note: CHA is dynamic and happens at runtime, depending which classes have been loaded. Loading new classes causes CHA to be performed again and if there are affected sites, the latter are to be deoptimized (and re-JIT again)

xxs

if the dispatches do use vtable they won't be inline and won't be faster. The real deal is inlining when necessary, which inheritance doesn't really prevent.

bitwize

Simple inheritance makes the class hierarchy complicated through issues like the diamond inheritance problem, which C++ resolves in typical C++ fashion: attempt to satisfy everybody, actually satisfy nobody.

The designers of StarCraft ran into the pitfalls of designing a sensible inheritance hierarchy, as described here (C-f "Game engine architecture"): https://www.codeofhonor.com/blog/tough-times-on-the-road-to-...

kragen

Simple inheritance doesn't have the diamond problem, because that requires multiple inheritance, which isn't simple. Smalltalk doesn't have multiple inheritance; I don't think SIMULA did either.

int_19h

Simula is strictly single inheritance (and no interfaces).

nine_k

The best implementation inheritance hierarchy is none :)

If you must, you can use the implementation inheritance for mix-ins / cross-cutting concerns that are the same for all parties involved, e.g. access control. But even that may be better done with composition, especially when you have an injection framework that wires up certain constructor parameters for you.

Where inheritance (extension) properly belongs is the definition of interfaces.

virtue3

really amazing read thank you.

steviee

This title is so wild when you read it without the context of software development...

juped

All of what we take for granted in modern computing architecture was invented as a performance hack by von Neumann in 1945 to take advantage of then-novel vacuum tube tech.

wonderwonder

Favorite part of this is that I had no idea if this article was going to be about biology, code or money. I love a good surprise

airstrike

I literally read it as money first, then code, but clicked thinking it may be biology too

hypercube33

My first assumption was something like ACL lists (though this could be for something like NTFS, or directory permissions) or even Firewall rules but I guess we all bring our background to assumptions

Lirael

[dead]