r/ProgrammingLanguages • u/gingerbill • 1d ago
CTTI is Exponential, RTTI is Linear
https://www.gingerbill.org/article/2026/09/02/ctti-is-exponential-rtti-is-linear/5
u/CommonNoiter 1d ago
In order to get the exponential cost of the generic you need to actually instantiate all those instances. This requires an amount of function calls proportional to the number of generic instances you want, so the total amount of codegen is linear with respect to the number of calls. This means that asymptotically CTTI and RTTI are the same cost as you vary the amount of calling code and only differ by the relatively large coefficient of the difference between the amount of codegen for a call and for a function body.
They are different asymptotically if you compare codegen amount by total amount of source code, as in that case both the number of instantiations and size of instantiated code are linear with respect to source code size, so CTTI can be forced to do a quadratic amount of codegen where RTTI would only do a linear amount.
3
u/slaymaker1907 1d ago
I think where things could actually get exponential is when you have generic functions that both generate new type(s) such as new containers and then call other such functions multiple times.
This may sound implausible, but I think it’s actually not that hard to do in C++, especially when you throw in constant “types”.
For example, imagine a print function which has the constant format string as part of the type to pre-compile the formatter. Now start doing things like having business logic functions which accept a format string, prepend their name for logging (possible at compile time in C++), and then call print.
How you can have such a program still be linear but have exponential instantiations is just by doing things like calling one other function with some condition else call some other function. The compiler has to create code for both branches unless it can prove that some branch is unreachable.
1
u/CommonNoiter 11h ago
I think if you allow your generics to support such features you usually end up with a type system that can perform arbitrary computations, then it's CTTI can perform computations at compile time and so can take forever (or at least as long as the type recursion limit is) while RTTI does it as runtime.
1
u/slaymaker1907 10h ago
I think the unique thing here is that you can get exponential code generation and not just that compilation can take an arbitrary amount of time.
4
u/apajx 1d ago
- Monomorphising is not the only way to specialize, you can internalize type codes and do all the same runtime-type shenanigans you want
- Generics need not be monomorphized so the title is click bait to begin with
0
u/gingerbill 1d ago
Monomorphising is not the only way to specialize...
Generics need not be monomorphized
Sure but I was specifically referring to parametric polymorphism, not "generics" in general, which is already too generalized of a term.
1
u/ultrasquid9 1d ago
Programmers know what you mean when you say "generics". If you still don't like the terminology, you can use "type parameters" as people have been doing for years now.
Frankly, if you want to be semantically correct with naming, a far more productive change would be changing "unsigned integer" to "natural" (an integer is by definition a potentially-negative number).
1
u/gingerbill 1d ago
When people commonly say "generics", they are referring to parametric polymorphism. That is actually the common meaning of that term.
1
u/yorickpeterse Inko 18h ago
CTTI is an exponential cost everywhere in the worst-case: semantic checking, code generation, and binary size. Instantiations go multiplicative in the general-case.
I think it's worth defining what "cost" here means. For example, if you have a generic function foo[T](value: T) that's instantiated for three different types, you don't have to type-check the body of foo three different times; instead you just check if those types are compatible with whatever T requires. There's definitely a cost to that, but depending on the implementation of your compiler it may be small enough that no matter the big-O factor it may not matter.
Unfortunately these are the costs so many programmers are most conditioned/trained to ignore [...]
Here I actually have a useful data point, one that does to some degree highlight that (at least in the case of generics) doing more at compile-time is usually the better option: Inko used to essentially group generics into buckets based on the shape (i.e. the size on the stack) of types, instead of specializing for individual types. This was recently removed because it actually turned out to slow down compile times. While I'm certain some of that could've been improved, the grouping of types essentially meant a lot more dynamic dispatch and thus a lot more methods that had to be compiled because they might be called as we couldn't statically determine that they never would be called (amongst other reasons).
So "specialize over types" isn't necessarily slower (in terms of compile times), it depends on a lot more than just that.
Outside of that I generally agree with preferring runtime type information for the purpose of reflection, as it usually allows for a lot more flexibility (e.g. building a REPL). But for more common patterns such as formatting types so they can be printed, I'm not a fan due to the amount of indirect calls/dynamic dispatch it will add.
1
u/gingerbill 18h ago
you don't have to type-check the body of foo three different times; instead you just check if those types are compatible with whatever T requires
That depends on the language. In languages like C++, Odin (my language), Jai, Zig, etc, you actually have to check the bodies BECAUSE they do not work on a type-class system. In this ilk of languages, languages like Go and Rust do not need to type check those bodies again, but then again, they are doing something else. Also if those bodies have any form of condition compilation based on the type of that parameter, then you do have to check.
So if you understand that context, then the rest of the article should make more sense.
1
u/matthieum 18h ago edited 17h ago
RTTI is Linear
Doesn't this widely depend upon what information is included in RTTI?
For example, in C++, RTTI can be used to perform cross-casts via dynamic_cast. This requires that every type encode the full hierarchy of interfaces it implements.
The size of this information is linear in the number of types, and linear in the highest number of interfaces implemented by a type... but it's the product of the two, so feels quadratic-ish no?
CTTI’s worst case is exponential, in three places at once (semantic checking, code generation, and binary size)
You're missing perhaps the most important place of all: i-cache.
Bloating the i-cache with virtually (but not quite) identical copies of the same function has a run-time performance cost, due to the ensuing cache-misses which stall execution.
1
u/gingerbill 17h ago
C++ doesn't really have any RTTI, and I would not call
dynamic_casta (good) example of that.I agree that the code to check that will be a quadratic runtime check, but it's also a check virtually no-one should ever be using since C++ is just badly designed everywhere. When I've had to do that kind of check in a company where
dynamic_castwas banned, we just stored an enum and the check was now constant.
19
u/initial-algebra 1d ago
The math makes no sense. Yeah, technically the worst case (more like pathological) scenario for monomorphization is exponential, but where does "best case N×K" come from? How can you even multiply the two quantities meaningfully?
Monomorphizing compilers only generate code for instances that you actually use. The only way I can think of to get exponential code size out of this is by putting all your N types in a big sum type and using that with K-ary dispatch (so you have a match expression with N branches, and each of those branches has a match expression with N branches, and so on, so you have a tree with Nᴷ leaves, each calling a different instance of the generic code). But, is that really common, or even possible to do accidentally in any existing language?
Also, you can have types for "type-erased blobs with RTTI" e.g. Rust's
dyntypes. So, monomorphization can be strictly more flexible, allowing the programmer to decide when to take the size/speed tradeoff.