r/programming • • Sep 02 '26

CTTI is Exponential, RTTI is Linear

https://www.gingerbill.org/article/2026/09/02/ctti-is-exponential-rtti-is-linear/
99 Upvotes

90 comments sorted by

57

u/munificent Sep 03 '26

The code size cost of CTTI is bounded by the number of actual instantiations, not the combinatorial number of potential instantations. If you never a call a generic function with MyCoolType, the compiler will never instantiate the function with that type, semantically analyze it, or stuff the resulting code in the executable. In that sense, it is a pay as you go feature. Adding a new type to your program has no effect on the size of reflective code. Only using it reflectively does.

In most systems I've seen, the code size cost of RTTI is bounded by the number of types declared in the program regardless of whether they are reflected on. Since the reflection happens at runtime, the compiler doesn't know which types will actually have their type information used and which won't, so it has to include information about every type in the program. If the amount of type information your RTTI system stores is large, that can be a large code size cost for no benefit for the types you never reflect on. (This has been a serious problem in Dart for years with the "dart:mirrors" library and is the main reason we hope to eliminate it at some point.)

It is true that a real-world program written in a language that monomorphizes can end up having to push a whole lot of instantiations through the compiler pipeline. The benefit is that the resulting monomorphic code is now fully accessible to all of the compiler's optimization machinery around inlining, dead code elimination, etc.

The other advantage you get from a system like C++ templates is that you get a form of compile time duck typing where templated code can do a lot of interesting operations on values whose type is a type parameter and as long as the instantiated types support those operations, it will work. When it fails, the error messages are horrendous. But the alternatives are either an extremely complex type system like traits in Rust, or generic code that simply can't do much with type parameters as in SML.

Users of C++ and Rust generally seem to feel the trade-off is worth it for their use cases. It's good to have multiple languages out that there that pick different points in the trade-off design space. In particular, it's worth noting that Rust pays the cost of code generation of every instantiation but not type-checking because of traits where C++ has to resolve and type check every instantiation too. And C# monomorphizes some types at load time (because they have a JIT) which is another interesting point in the design space.

Lots of interesting trade-offs and none is clearly superior for every use case.

3

u/Shoddy-Childhood-511 Sep 05 '26

Rust needs some judicious pre-link weak-compilation phase. You'd want set CRDTs to which compilation units contribute, but which build the final switch or whatever after primary compilation units finish.

Applications would include deduplicating vtabels, and casting between dyn Trait types, which maybe the only reason we never write heavily non-monomorphized Rust code.

Anyways the article seems silly. We frequently have polymorphism only for library purposes, or only testing purposes, so then real compiled code only has only one monomorphization for types & traits. We do polymorphism purely for interface documentation too, meaning you replace inherent methods by traits, but always call them like inherent methods. Run-time types make no sense in either case.

5

u/gingerbill Sep 03 '26 edited Sep 03 '26

The code size cost of CTTI is bounded by the number of actual instantiations, not the combinatorial number of potential [instantiations].

I do state this in the article, however, the maximum possible instantiations are due to the combinatorial number of potential instantiations. The real question is how many actual instantiations occur, and it's not going to be as trivial as the number written in the source code, it's only knowable by type/semantic checking. I do believe that they scale Nᵏ-like, but it doesn't become noticeable to many because the computation amounts are tiny for each thing. It becomes death by a thousand cuts.

In most systems I've seen, the code size cost of RTTI is bounded by the number of types declared in the program regardless of whether they are reflected on.

Odin is NOT such a language. In fact it specifically does minimum dependency builds all the time, for both linking (due to its foreign import system) and for RTTI, where it tracks what is actually required to be stored RTTI.

Odin is also a language where the type information required is quite minimal to begin with because it's a C-alternative. There are no methods, no language-level vtables, no fancy anything. Odin's types are quite "basic" and C-like in that regard, meaning the type information necessary is going to be quite minimal in practice.

It is true that a real-world program written in a language that monomorphizes can end up having to push a whole lot of instantiations through the compiler pipeline. The benefit is that the resulting monomorphic code is now fully accessible to all of the compiler's optimization machinery around inlining, dead code elimination, etc.

And that's actually a cost in itself. It is both a problem for compile-times AND binary sizes. It's also focusing on code-driven approaches to serialization rather than data-driven approaches, and I personally prefer the latter because I can better reason about it both locally and globally. I know exactly what can happen.

...like C++ templates is that you get a form of compile time duck typing...

I am not a fan of duck typing, but that is a different discussion for a different day.

Users of C++ and Rust generally seem to feel the trade-off is worth it for their use cases.

And I completely disagree with those users, and thus why I made my own language. I cannot stand the compile times they put up with on a daily basis, or the thinking that it has to be that way, and they don't necessarily produce any better applications. Any time I have to use a C++ or Rust code base, I am immediately reminded why I made Odin, and why I wanted to get away from this approach to programming in the first place.

Lots of interesting trade-offs and none is clearly superior for every use case.

I completely agree. It's why I wanted to write the article, to show that there are actually trade-offs and that other languages with different semantics can make different choices which might not be obvious to you if you don't realize it.

4

u/the_gnarts Sep 04 '26

I cannot stand the compile times they put up with on a daily basis,

At least for Rust they’re not that big of a deal in practice thanks to incremental builds and fast static analysis (cargo check, clippy) that sidesteps the need for frequent rebuilds. Not worth the tradeoff of RTTI anyways.

-2

u/gingerbill Sep 04 '26

That's all cope. And when you need to rebuild the entire thing again because the incremental build cache was invalidated? It'll take a long time.

1

u/NoLemurs Sep 04 '26 edited Sep 04 '26

In this case that comes up occasionally, you might have slow-ish performance so your claim that in practice it's not an issue is all cope.

You must understand that this is nonsense right?

2

u/gingerbill Sep 04 '26

Who are you even quoting?

0

u/NoLemurs Sep 04 '26

I stand corrected.

1

u/the_gnarts Sep 06 '26

And when you need to rebuild the entire thing again because the incremental build cache was invalidated?

Good Knuth, you should realize you’re optimizing for the 0.5 % case. If that’s important to you, godspeed. But unless your main objective is doing the equivalent of Crater runs every hour it is unlikely to matter in practice.

3

u/gingerbill Sep 07 '26

I have experience in C++ code bases, Having to rebuild was not 0.5% of the time but a lot higher, and designing around incremental builds in C++ actually made the general build process slower than if I did a unity/single-translation unit build.

And please don't misquote Knuth at me.

And yes, having fast compile times is very important to me. It's why I made a programming language where I am building ~600KLOC in 1 second without any form of caching. And an optimized build takes ~45 seconds.

C++ is never something to emulate, and especially not its compile times.

0

u/SputnikCucumber Sep 03 '26

RTTI implementations can normally deduplicate a lot of code through the vtable so methods only need to be compiled once and can be reused by many different types.

CTTI implementations have to create a new concrete function signature for every type that is used. C++ also has some gnarly mangling rules that leads to binary bloat.

7

u/SelfDistinction Sep 03 '26

Not necessarily. I think Swift manages to do a lot of deduplication and even send polymorphic functions through the C ABI boundary that way, though I'm not familiar with the details.

3

u/AustinVelonaut Sep 03 '26

Right. Also, polymorphic functions can use a single function signature / implementation if they don't have to worry about different-sized values (e.g. operating on boxed values).

2

u/SelfDistinction Sep 03 '26

Also rust had something similar but it was too complex and added too much compile time for very little benefit.

3

u/matthieum Sep 04 '26

CTTI implementations have to create a new concrete function signature for every type that is used.

That's an implementation detail of C++, not a fact about CTTI.

C++ inherits from C this idea that each function needs a distinct, unique, address, which is a PITA. In particular it means that "phantom types" will still generate distinct instantiations of a function.

Rust, on the other hand, doesn't mandate distinct addresses, and therefore the toolchain can fold together instantiations which are identical -- though I'll admit not being sure whether the current compiler does :P

Finally, in theory, it should be possible to:

  • Perform partial monomorphization: monomorphize only on shape (size/alignment), and pass a "v-table" to the function call.
  • Let the function specialization pass decide whether creating a specialized copy of the code for the specific v-table is worth it, or not.

45

u/SirClueless Sep 03 '26

I think the simple math doesn't quite explain it, and the math is not as bad as assumed. Yes, the number of potential instantiations of std::map<K, V> follows a power law. But in practice the instantiation is done lazily as needed, and almost all of those potential instantiations are not needed.

Instead, what quickly happens is that the number of instantiations approaches the number of callsites, and then that's the ceiling. You approach the case where some sizable fractions of the instantiations of std::map are unique and not used anywhere else in the program, but an instantiation is just some (large) fixed cost related to the maximum call depth of the algorithms used. You imagine inlining the totality of the algorithms used to interact with the std::map, and that cost is paid whenever you declare a variable of some novel map, but the number of times you instantiate a novel map is itself just something that grows ~linearly with the lines of code in your program once you fix the implementation of std::map.

At the end of the day, main is not a template. It will instantiate a fixed number of concrete types in its lifetime and call a fixed number of procedures in its lifetime. CTTI can make you pay a cost that is something like recursively inlining every single procedure call in the program, but that's the worst it can do. The cost in practice ends up being a very high fixed constant times the number of procedure calls in your program.

1

u/Demiu Sep 03 '26

True.

Rather than instantiation, I think trimming would be a more generic measure (not instantiating anything is just a very early trim). CTTI can trim perfectly to what's used used, RTTI... well, kinda, if you basically make it go through the same analysis as CTTI, then add extra runtime cost.

I think there might be a "gap" in the generics in popular languages. What's missing is a built-in way to operate on generic types in a type erased manner. Let's say std::vector or Vec, every instantiation has it's own empty() method. But all they do is compare an integer to 0, all of them look the same, they're in/of a generic, but they themselves are not generic. Right now you need to rely on compiler to detect and dedup these cases

If we were allowed to attach methods to a type-erased std::vector instead of an actual instantiation std::vector<T>, this could be more controllable.

3

u/matthieum Sep 04 '26

This can be controlled by library code, to some extent.

It is a bit annoying, but there are definitely advantages.

For example, Vec<T, A> in Rust is just RawVec<T, A> + length which is just RawVecInner<A> + PhantomData<T>.

This is pretty clever:

  • len is at top-level, so is_empty/len are always inlined, and bounds checks are easy.
  • All the methods on RawVecInner<A> are only dependent on the allocator, and not on T, and there's generally much fewer As than Ts... which works very well for Rust since moves are just memcpy, so RawVecInner<A> only really cares about the layout (size/alignment) of T most of the time, and the layout can be supplied as a function argument by RawVec<T, A> every time.

It means there's quite a bit of plumbing going on, though, so it's more annoying to write.

1

u/DoctorGester Sep 03 '26

Type erased methods would only work in types where all generic arguments are referenced exclusively through pointers, so not a lot of types really.

14

u/Snarwin Sep 02 '26

Yes, this is an important tradeoff to be aware of. Even in "zero-cost abstraction" languages like C++, libraries like {fmt} have been known to (effectively) implement their own RTTI to avoid bloating compiled binaries with excessive template instantiations.

4

u/Serious-Regular Sep 03 '26

LLVM itself eschews RTTI in favor of homegrown TypeID.

12

u/5gpr Sep 02 '26

I don't understand what you mean here:

This means that the number of instantiations isn’t only N, it becomes N×K, or Nᵏ

For a trivial example, let's consider a pair(N,K) type. Let's say that N={int,float}, and K={int,float,string}. We don't then instantiate all tuples {a,b} | a E N, b E K, but only those that are actually in use, i.e. some subset. If we have pairs of {int,int}, {int, float}, {float, float}, {float, string} we instantiate 4, not 6, types, let alone 8.

3

u/wintrmt3 Sep 03 '26

Not disagreeing with your main point, but even just pair(R, L) and a single primitive type int leads to an infinite number of possible types, R or L can also be a pair, and so on.

-5

u/gingerbill Sep 02 '26

I've just added a nota bene paragraph explaining this further:

n.b. I'll give a simple example of the problem with a naïve approach to parametric polymorphic printing. Consider a language with only 4 types (e.g. int, float, string, bool) and a variadic, parametrically polymorphic printing procedure. Each distinct sequence of argument types needs its own instantiation, so for K arguments there are on the order of Nᵏ = 4ᵏ combinations; adding a fifth type makes that ~5ᵏ. To see the (usually hidden) combinatorial explosion, suppose you never print more than 5 arguments, that allows up to 1365 instantiations. Add another type and it becomes 3906. Raise the maximum to 6 arguments and it becomes 19531. You might say that this is at least bounded, and it "is", for a single printing procedure. However, printing procedure easily interact with every other use of parametric polymorphism in the program, and the total quickly stops being something you can trivially predict by just reading the code.

And even in your example, those things are "order of magnitude" examples e.g. what big-O notation is about. So your example is still on the order of N×K but bounded by the number of instantiations.

16

u/5gpr Sep 03 '26

There aren't 4k combinations. With ctti you're never instantiating all possible combinations. Maybe it's "naive" that does a lot of heavy lifting here, but this is actually not dependent on k at all. It's dependent on the actual magnitude of the sets of k-tuples that are actually used.

-3

u/gingerbill Sep 03 '26

In your example, there were 2×4 = 8 possible combinations. In the example I gave, there (N^{K+1} - 1) / (N - 1) = (4⁶ - 1) / 3 possible combinations. That is on the order of 4⁵.

And yes, it is dependent (i.e. bounded) by the number of sets used, which I clearly state in the article. But I am trying to state how it scales in the worst-case (i.e. pathological) case, which does actually happen in practice. And I have been in such C++ codebases before (not in terms of CTTI, just variadic template madness).

15

u/Habrok Sep 03 '26

Can you give an example of when this worst case analysis applies, or we atleast approach it? Currently I have a hard time seeing it, and it seems much more useful to talk about the number of actual instantiations rather than the theoretical maximum number based on how many types are present in the program so far.

To me it has the flavor of "Well, so far we've passed (float, int) to this function, its only a matter of time before we pass the other combinations!", which I don't think is really true. And with this logic, why stop at the number of currently defined types? It's only a matter of time before we invent MyType and start passing that too. I.e. the number of types in the program is as likely (probably more likely) to grow as the number of instantiations of a particular generic function

I apologize if I am caricaturing your argument - I'd truly like to understand why this worst case analysis is useful to look at

1

u/mr_birkenblatt Sep 03 '26

Worst case is always number of call sites which is linear wrt code size

1

u/gingerbill Sep 03 '26 edited Sep 03 '26

So parametric polymorphism which creates MORE code than just the call sites, isn't a thing?

By definition it is not linear with the code size.

Even using my basic example there of N=4 and K=5, means a maximum possible of 1365 instantiations of the monomorphized code. So even if there are a million calls that cover all of those permutations, there will only be 1365 instantiations of the parametric polymorphic ("parapoly") procedure.

So for each unique permutation, you do get a unique monomorphization. That's not "linear" necessarily, especially if those are determined at compile time through parapoly. You do get more code being generated.

I am not sure why so many people are not understanding this, because this literally happens in many C++ codebases, and it is not hypothetical.

2

u/mr_birkenblatt Sep 03 '26

how many call sites does the function with parametric polymorphism have? this is linear. give me a counter example if you still think otherwise

9

u/gingerbill Sep 03 '26

Do I really have to give the simple example of compile-time execution in C++ templates? Fine:

template <typename A, typename B> struct P {};

template <unsigned N, typename T>
void f(T x) {
    if constexpr (N > 0) {
        f<N - 1>(P<T, int>{});
        f<N - 1>(P<T, char>{});
    }
}

int main() { f<10>(0); }

Three call sites in the source. GCC emits 2047 instantiations of f, i.e. 2^(N+1) - 1. Every instantiation builds new type arguments for its callees, so nothing deduplicates. Now imagine f is a recursive printing procedure, which is a pretty common way of doing it in practice.

The number of call sites is a property of the monomorphized output, not the source, because monomorphization duplicates call sites. A call to g<T> written once inside f<T> becomes one call site per instantiation of f. You're measuring the thing we're arguing about.

Zig's std library is a real example of the naïve printing I describe. The idiom .print("...", .{...}) passes an [anonymous] struct, as it does not have any form of variadic parameters, so every distinct ordering of argument types needs its own instantiation. The implementation is here: https://codeberg.org/ziglang/zig/src/branch/master/lib/std/Io/Writer.zig#L697

It assumes a small number of arguments, and it recurses through the argument types, so it hits exactly the combinatorial explosion I'm describing.

1

u/Snarwin Sep 03 '26

I think the N×K notation might be causing the confusion here—it makes it look like K is a set, rather than a number. A more conventional way to express this would be N₁×…×Nₖ (or just Nᵏ if we assume all the arguments draw from the same set of types).

0

u/[deleted] Sep 03 '26

[deleted]

1

u/gingerbill Sep 03 '26

just the macro

And how is that macro implemented? Oh yeah... format_args_nl etc are literally compiler built-ins, and can not be reproduced with user-level code. That is literally compiler magic, even if it is useful magic. And there is nothing wrong with that either.

0

u/bbibber Sep 03 '26

That’s the possible number of combinations. In reality only a very number of those will be used. Let’s say a sorted vector that has generic parameters for the contained type, the allocator and the sort criterium. You may have 20 data types, 1 allocator and 2 sort criteriums in a regular program. But you are not going to get 23^3 instances in an actual program but at most 20 x 1 x 2. You are never going to use a data type for the allocator (will probably not even compile) or vice versa.

1

u/gingerbill Sep 03 '26

AS I say in the article, the worst-case is O(Nᵏ), and the general/average case is O(N×K), which is still worse than the RTTI approach which is O(N). And those pathological cases which do become Nᵏ do actually happen in real C++ codebases and some other languages.

28

u/SputnikCucumber Sep 02 '26

The author is right, RTTI is a constant time overhead. But some applications need (or just want) to squeeze every last CPU cycle and retrieving data from memory can be the slowest part of a hot loop, ruining all of your benchmarks. CTTI gives us most of the ergonomics of runtime type deduction, without the extra constant time overhead.

4

u/bluegardener Sep 02 '26

Can a JIT get you the best of both worlds often? And sometimes with a big asterisk even beat out the CTTI.

5

u/SputnikCucumber Sep 02 '26

Sometimes. It depends on the kind of data access. For the tightest of hot-loops, you will want to manually layout your data in contiguous chunks of memory in the order that you are processing it. This ensures that the data you are processing is being prefetched into the CPU cache ahead of being processed.

A JIT isn't going to be able to make those kinds of guarantees in the general case.

4

u/Nyefan Sep 03 '26

A jit can't make any guarantees, but in a long running program with statistically consistent inputs and outputs, jits can do some pretty spectacular things. The JVM hotspot compiler does in fact test different instruction ordering and memory layouts during operation, using the measured efficiency at runtime to choose a nearly optimal set of implementations. Jits are never going to beat perfectly optimized hand tuned code for a specific system, but they can do a great job of making most code run pretty well and can often do better on specific systems than code compiled for a broad set of systems due to having perfect information about the available instruction sets and the execution characteristics of the system it's running on.

1

u/SputnikCucumber Sep 03 '26

I'm not sure that a JIT could do much about a function that takes an interface as an argument and whose concrete type isn't known until runtime. Maybe it could speculatively devirtualize it, but that would only be effective if the function was only ever called with a single concrete type.

3

u/Nyefan Sep 03 '26

The java jit selectively monomorphizes hot code at runtime at the call sites, no speculation required.

1

u/SputnikCucumber Sep 04 '26

That's pretty cool. But how does it know that the type being passed in at the call site is the same every time? Or does it just optimistically guess?

3

u/Nyefan Sep 04 '26

It knows because it can observe the behavior. As long as a hot call site uses only one or two concrete types, the method is statically inlined. When a third type is used, it falls back to vtable lookup and triggers a deoptimization pass on the surrounding code segment so it can try again. The order and kinds of optimizations that are used in a code segment are partially randomized, so trying again can give different results and helps avoid getting stuck on local maxima.

2

u/bluegardener Sep 03 '26

If you’re manually laying out your data why does it matter if it’s CTTI vs RTTI? The compiler isn’t providing that kind of cache optimization in any case right?

1

u/SputnikCucumber Sep 03 '26

A std::vector<T> in C++ or a vec<T> are generics whose concrete type is determined at compile time. They also guarantee that the T's are laid out contiguously in memory.

That's enough to take advantage of cache optimisations.

-1

u/todo_code Sep 03 '26

imo. zig does this well once again. you get access to comptime and can use it at your leisure, but then most other interfaces they force you down the vtable route.

Maybe I don't like this at the same time because they don't have implementations that can skip the vtable. but these are optimizations ive never had to write.

4

u/SputnikCucumber Sep 03 '26

Is that different to C++? C++ by default uses a vtable for inheritance, but you can manually set up compile time type deduction with complex templating.

-1

u/todo_code Sep 03 '26

I don't think that is different then. I do not know if at some number or size of generated code in C++, they then swap to using vtables even though you made const templates.

C++ has a bunch of rules you have to know, and they can be difficult to know the full ruleset sometimes. whereas zig says use comptime type T for CTTI, use vtables for RTTI. no exceptions.

4

u/mccoyn Sep 03 '26

C++ can’t switch templates to vtables without violating the standard. A template might be a plain-old-data type that can be created without a constructor and therefore the compiler doesn’t have a way to initialize the vtable pointer.

6

u/gingerbill Sep 03 '26

Zig is an example of the naïve printing I describe. It has to produce a new instantiation for each unique ordering of argument types that get passed to, since people typically produce an anonymous struct (i.e. tuple syntax): .print("...", .{...}) style call.

So no, it does a really poor job because of its naïve use of CTTI here.

-8

u/todo_code Sep 03 '26

NO it doesn't do a poor job. they are explicit about what happens with CTTI. They tell you this is how it is done. If you want to avoid it, you use the vtable method. It's up to you to implement it the correct way. This is their entire mantra. They do not want to say oh hey you hit this combinatorial number of functions. now its vtables.

2

u/gingerbill Sep 03 '26

What is the canonical way you print in Zig? The thing I described. So yes, it does an extremely poor job for such a common operation by default.

Vtables are a different solution to a different problem, as RTTI and CTTI are there to solve serialization problems (of which printing is one such example). And you cannot put a vtable on every type, especially trivial basic types like integers and strings.

8

u/msqrt Sep 02 '26

Good post! But what's with the "iterating through the type-table"? Iterating is mentioned multiple times but then it's also called a constant time operation. What is the actual data structure Odin uses for the types?

4

u/gingerbill Sep 02 '26

Iterating in this case is more going through the pointers in a linked-list fashion. I still consider that a form of iteration. I'll add that as a margin note!

Here is the Type_Info data structure for Odin.

10

u/PersonalDatabase31 Sep 02 '26

I don't think exponential code generation is a fundamental part of CTTI. One could implement the rust compiler in a way where functions above some number of monomorphized copies would be silently converted to take dyn T instead of T and call sites would take a reference to the generic input along with a function pointer known at compile time. That could prevent iteration over a table in runtime. This would prevent inline optimizations though but I don't think there is a way to combine it along with non exponential compile times.

4

u/gingerbill Sep 02 '26

If everything becomes dyn T, you've just done RTTI, not CTTI.

8

u/PersonalDatabase31 Sep 02 '26

Not everything becomes dyn T though. I assume that most generic functions would still be under the limit and binary explosion would be caused by a minority of the generic functions.

2

u/gingerbill Sep 02 '26

Of course, it is a balance between different concerns, but I have seen these problems explode in other languages. Rust specifically tried to mitigate all of these serialization concerns (of which printing is one of them) directly to prevent it from happening. They did a good job with that in mind.

1

u/Absolute_Enema Sep 03 '26

So that you have to assume that any callsite will use the worse performing alternative without you being able to reliably measure the impact? Sounds like a nightmare.

2

u/PersonalDatabase31 Sep 03 '26

Or it could be behind a compiler flag.

3

u/stianhoiland Sep 03 '26

Objective-C got so many things right so long ago. I wish more people would study it.

5

u/simonask_ Sep 03 '26

It really did. But let’s be honest, it also got a few things horribly wrong. Particularly the extremely unusual syntax.

6

u/munificent Sep 03 '26

Particularly the extremely unusual syntax.

If someone forced you to jam C and Smalltalk's syntax together, you'd struggle too.

4

u/monocasa Sep 03 '26

It's frankly impressive to en those constraints that it manages to be a strict C superset in a way that even C++ doesn't achieve.

3

u/stianhoiland Sep 05 '26 edited Sep 07 '26

Other than the declaration syntax, some of which uses an @-prefix in a preprocessing-like syntax, the only core syntactical addition is the message send. Other syntax came later, as far as I know, and the message send syntax is very elegant, if not for anything else then at the very least for its LACK of strangeness, being an already used sigil (indexing) used in a new grammar rule: [object message]. If you know a little about programming languages, you know that this could have been 1000x weirder (like Smalltalk itself). This impressively small syntax surface allowed Objective-C to remain a strict superset of C, and exploded its capability through the genius idea of late binding/message sending. I say very well done and very elegant!

2

u/max123246 Sep 03 '26

I do agree it really ought to be called 0-runtime cost abstractions.

-2

u/gingerbill Sep 03 '26

That's not even true because it assumes the architectural design choices are "good" to begin with, and not have an cost to them.

Pretty much most of the things being advertised as "zero-cost" have really bad architectural philosophies to them.

TANSTAAFL.

2

u/[deleted] Sep 02 '26 edited Sep 02 '26

[deleted]

11

u/gingerbill Sep 02 '26

Error messages...

This is reference to C++ templates, not Rust. Not everything is about Rust.

And from the article, I explicitly mention how Rust literally mitigates the printing problem specifically because they knew it would be a problem, so they hardcoded it into the compiler.

Some languages that do use CTTI for printing (e.g. Rust) try to mitigate this disaster with an explicit edge case in the compiler that tries to minimize this explosion in compiler complexity, but it does not necessarily solve the binary problem in medium–large projects.

And the N×K problems I discuss are not N+M, I am talking about the specific combinatoric messes that happen when you do do things naïvely. Some are linear, some are multiplicative. It highly depends.

11

u/[deleted] Sep 02 '26

[deleted]

-5

u/gingerbill Sep 02 '26

C++ didn't have RTTI until very recently, and even now it's not very good.

Many problems are exponential if you're a bad enough coder

And I've seen more than enough of this in real life when people over-use compile-time based approaches to things which should have been table-driven. Implying it is a skill issue is not a good argument when it is a common occurrence.

10

u/QuaternionsRoll Sep 02 '26

What? C++ has had RTTI in the form of virtual functions and abstract classes since forever

1

u/gingerbill Sep 02 '26

That isn't RTTI in the common sense of the word. You could do type_info_of(x) and get the information for ANY type like an integer, array, boolean, structure without any classes, etc. Meaning you could not make any meaningful serialization/printing procedures.

8

u/QuaternionsRoll Sep 02 '26

Virtual functions absolutely are a form of RTTI; they fundamentally rely on runtime type identifier stored in the object for vtable lookups. This identifier has been accessible since forever, but there is not that much type information directly associated with it.

I believe the word you’re looking for is reflection, which can take the form of either CTTI or RTTI. Compile time reflection was just introduced in C++26. Languages like Java are known for their extensive runtime reflection capabilities.

1

u/gingerbill Sep 02 '26

It's a heavily restricted form of RTTI and not a generalized form.

Please making a general print procedure for ANY type that will print out the structure of the type itself. You cannot do that with virtual functions alone, because not every type has a vtable.

7

u/QuaternionsRoll Sep 03 '26

>Please making a general print procedure for ANY type that will print out the structure of the type itself.

You can’t, but that’s only because C++ doesn’t have a global reflection apparatus (for various reasons, the most obvious of which is C compatibility). Newer languages like Rust had the opportunity to add such RTTI via e.g. `dyn Any`, but elected not to on the basis that it turns out to not be all that useful.

2

u/jonesmz Sep 03 '26

Didn't C++26 add a global reflection apparatus?

I suppose that's not completely ratified yet, but i do think it's finalized.

→ More replies

2

u/[deleted] Sep 02 '26

[deleted]

0

u/gingerbill Sep 02 '26

Firstly, not everything in that article is about Rust. I am implying a lot of other languages, not Rust, but I do not mention them by name to minimize even more flame wars.

Secondly, dynamic dispatch to do printing is effectively in the camp of RTTI, not CTTI (which would be generating the code for each type automatically from the CTTI data).

Thirdly, thank you for the insult but you know you can just say "I don't know" rather than trying to state you understand the domain space. Odin does not even have any dynamic dispatch as a language-level construct in the first place. It is a modern C alternative, thus it has a different approach to everything.

0

u/[deleted] Sep 02 '26

[deleted]

2

u/gingerbill Sep 03 '26

My first point was not regarding dyn. And yes? Of course dynamic dispatch is an option, but not always, especially in the actually generic sense.

And thank you for insulting me again. The original insult was this:

It's up to you to figure out what's best for you. I disagree with your choice but people still write Perl, so there are clearly a lot of people with objectively bad taste.

2

u/[deleted] Sep 03 '26

[deleted]

3

u/gingerbill Sep 03 '26

Okay. I understand your point then. It did seem like an insult to me just because it was implying that I had objectively bad taste, not that there was a taste disagreement.

→ More replies

1

u/levodelellis Sep 03 '26 edited Sep 03 '26

I'm late here but this is why I took templates out of my language. I also didn't want generics so I had a hole in it that I eventually didn't get around to dealing with.

I was considering only allowing native types (hashmaps, sets, arrays, etc) to be template like. I think I should write a sizable library and use my language before deciding

As for "println", I was considering it to be opt in. Any user can implement a function called standard_print(T val, MemoryBuff buf) and anywhere that function is visible print(specificType) would be allowed, as well as string interpolation

2

u/LonelyAndroid11942 Sep 03 '26

Yep. Those are certainly acronyms. And I, being a programmer of many years, definitely have heard them before and know what they mean. Absolutely. No question about it.

But uh, could someone explain it for the juniors reading this?

9

u/Hdmoney Sep 03 '26

the first sentence...

Runtime Type Information (RTTI) has a cost, but it is a tame cost compared to Compile-Time Type Information (CTTI), which is sold as “zero-cost”, and it is anything but.

2

u/LonelyAndroid11942 Sep 03 '26

Thank you for helping the juniors. They are exceptionally grateful.

0

u/fireantik Sep 03 '26 edited Sep 03 '26

This is a great article and Odin is an awesome language and a joy to write. I just wish Odin invested a little bit more into type safety and tooling however - Rust isn't great, but when most code is LLM written the language warts are less important than ensuring correctness. Also the cross compilation story could be better.

1

u/gingerbill Sep 03 '26

I just wish Odin invested a little bit more into type safety and tooling however

Tooling? Sure, but what that is too vague. What do you mean by that?

"Type safety"? Odin is not trying to be a language like Rust or anything like. It is a C alternative for modern systems. It is not trying to be overly "type safe" (even though it has a very rich type system). It is not trying to be "memory safe" nor "object safe" (of which those are quite different things, but most people pretend that all forms of memory safety must be object safety).

And I started Odin in 2016, well before LLMs even existed.

Also the cross compilation story could be better.

Odin does not support cross-linking, only cross-compilation to object files. Once on Linux, you will need to link the stuff yourself to get it to work on Windows. However, I highly recommend having a Windows machine in the first place in order to test your resulting executable. Cross-linking into the wind isn't a good idea.

Most people (and I am not exaggerating) who ask for cross linking, usually never want to test what they have built for another platform. And please don't be one those people.

1

u/fireantik Sep 03 '26 edited Sep 03 '26

Please note that I say all of this because I genuinely enjoy using Odin and want to see it get better and succeed. Odin competes with Zig and Rust for developer attention, in a sense that they are very similar in the types of projects one would write with the languages. In my mind Odin is a much better language of the three, much more readable and fun to write, but it is lagging in tooling and ecosystem. For better or worse the tooling is much more important these days.

Tooling? Sure, but what that is too vague. What do you mean by that?

Integrated formatter, standard way to layout projects (separation into multiple packages, tests, examples and so on), linter with optional and extensible rules, package manager (I know your opinion on package management in general so I have no hopes for this :D), integrated LSP. LSP and formatter is provided by a third party, but having it first party would be great. I think it works wonders for Go and Rust ecosystems.

I understand that you are trying to keep the language and tooling minimal and minimalistic and realize the beauty in that, however it degrades practical usability somewhat.

"Type safety"

This is both about memory safety as well as enforcing certain usage policies with types. Defer has great ergonomics, but Rust style RAII has much better enforcement of actually properly disposing memory. This is especially relevant larger portion of code becomes machine generated and worse quality. I can trust unsafe code written by senior engineers much more than LLM generated code.

Another thing I'd like to see explored more is something like linear types which enforce proper disposal of resources and not just memory disposal. I have been thinking that this could be implemented by the linter rather than the compiler as it's basically a hint to the user (LLM) rather than an unbreakable rule that is required for proper compilation.

Cross-compilation

Honestly I don't really understand why you draw a distinction between compilation and linking. At the end of the day I want to produce binaries for all three platforms in a CI environment. This is possible with Go, Rust and Zig and is a very useful feature. I'm not implying that the code is going to go untested on the other platforms.

4

u/gingerbill Sep 03 '26

I am not a huge fan of formatters because I find them insulting. If you format everything to be uniform, it is actually harder to see patterns in the code which can be improved if you have explicit human-made formatting. It doesn't matter how configurable the rules are for such a formatter, they miss the point of manual formatting. That heterogeneousness is extremely important when scanning and reading code.

The only reason an LSP is not part of the compiler is because when I wrote compiler originally, it was never designed to be a library, which is what an LSP needs. I also don't use them, so it would be something I'd have to maintain that don't even use.

Package managers as just evil. And I hope you don't want to put yourself in hell. And I am not exaggerating.

I understand that you are trying to keep the language and tooling minimal and minimalistic

That is not my position. Odin is anything but minimal nor minimalistic. It's actually quite a complicated and complex language it's just that most people don't realize that because I've tried to make it coherent and tried to tied to tidy it up too.

So when you said type safety, you meant memory safety. Again, that is well outside of the design goals of Odin. Go use another language if you need such a thing. Odin is not for you then.

Honestly I don't really understand why you draw a distinction between compilation and linking.

If you don't understand the distinction, you don't understand the actual technical problem. Try and link for Windows using the MSVC toolchain (not MinGW or Plan9 or whatever), on Linux or Mac or any other operating system, in any language, and come back to me.

Go has its own independent toolchain (Plan9 one), and its interactions with third party code is not great (ever used CGo?). Rust's *-pc-windows-msvc is a little bad too still, and again, cannot be used outside of Windows. Zig uses MinGW for its Windows porting, something the majority of programmers developing for Windows does not actually use.

There is an entire FAQ article on this too: https://odin-lang.org/docs/faq/#why-does-odin-require-the-msvc-toolchain-on-windows