r/programming • u/gingerbill • 1d ago
CTTI is Exponential, RTTI is Linear
https://www.gingerbill.org/article/2026/09/02/ctti-is-exponential-rtti-is-linear/43
u/munificent 1d ago
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.
4
u/gingerbill 1d ago edited 1d ago
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 importsystem) 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.
1
u/the_gnarts 5h ago
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.0
u/gingerbill 3h ago
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.
0
u/SputnikCucumber 1d ago
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.
3
u/SelfDistinction 1d ago
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.
1
u/AustinVelonaut 18h ago
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).
1
u/SelfDistinction 18h ago
Also rust had something similar but it was too complex and added too much compile time for very little benefit.
25
u/SputnikCucumber 1d ago
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.
3
u/bluegardener 1d ago
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 1d ago
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.
3
u/Nyefan 1d ago
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 1d ago
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 19h ago
The java jit selectively monomorphizes hot code at runtime at the call sites, no speculation required.
1
u/SputnikCucumber 5h ago
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?
2
u/bluegardener 1d ago
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 1d ago
A
std::vector<T>in C++ or avec<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.
-2
u/todo_code 1d ago
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.
3
u/SputnikCucumber 1d ago
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 1d ago
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/gingerbill 1d ago
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.
-7
u/todo_code 1d ago
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 1d ago
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 1d ago
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?
3
u/gingerbill 1d ago
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_Infodata structure for Odin.
8
u/5gpr 1d ago
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.
2
u/wintrmt3 1d ago
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.
-7
u/gingerbill 1d ago
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 forKarguments there are on the order ofNᵏ = 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×Kbut bounded by the number of instantiations.14
u/5gpr 1d ago
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 1d ago
In your example, there were
2×4 = 8possible combinations. In the example I gave, there(N^{K+1} - 1) / (N - 1) = (4⁶ - 1) / 3possible combinations. That is on the order of4⁵.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).
10
u/Habrok 1d ago
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
0
u/mr_birkenblatt 23h ago
Worst case is always number of call sites which is linear wrt code size
0
u/gingerbill 22h ago edited 22h ago
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=4andK=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.
1
u/mr_birkenblatt 22h ago
how many call sites does the function with parametric polymorphism have? this is linear. give me a counter example if you still think otherwise
5
u/gingerbill 21h ago
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 imaginefis 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 insidef<T>becomes one call site per instantiation off. 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#L697It assumes a small number of arguments, and it recurses through the argument types, so it hits exactly the combinatorial explosion I'm describing.
1
0
1d ago
[deleted]
1
u/gingerbill 1d ago
just the macro
And how is that macro implemented? Oh yeah...
format_args_nletc 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 1d ago
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.
2
u/gingerbill 1d ago
AS I say in the article, the worst-case is
O(Nᵏ), and the general/average case isO(N×K), which is still worse than the RTTI approach which isO(N). And those pathological cases which do becomeNᵏdo actually happen in real C++ codebases and some other languages.
9
u/PersonalDatabase31 1d ago
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.
1
u/gingerbill 1d ago
If everything becomes
dyn T, you've just done RTTI, not CTTI.8
u/PersonalDatabase31 1d ago
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 1d ago
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 1d ago
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
2
u/stianhoiland 1d ago
Objective-C got so many things right so long ago. I wish more people would study it.
3
u/simonask_ 1d ago
It really did. But let’s be honest, it also got a few things horribly wrong. Particularly the extremely unusual syntax.
3
u/munificent 1d ago
Particularly the extremely unusual syntax.
If someone forced you to jam C and Smalltalk's syntax together, you'd struggle too.
2
u/monocasa 19h ago
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.
2
u/max123246 1d ago
I do agree it really ought to be called 0-runtime cost abstractions.
-5
u/gingerbill 1d ago
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/torsten_dev 1d ago edited 1d ago
Error messages measured in kilobytes and require a degree in Egyptology to decipher
My guy has clearly not used the language he criticizes enough to form an educated opinion.
Printing in O(N) space and constant time is fine.
The amount of heterogeneous container types is at most linear in the amount of code you write and they're (practically) bounded in size anyway.
The examples of NxM problem he cites are really N+M if you have an intermediate type with dyn dispatch.
11
u/gingerbill 1d ago
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×Kproblems I discuss are notN+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.8
u/torsten_dev 1d ago
You can do inheritance and vtables in C++ and it's recently gotten reflection so you can automatically derive serialization machinery, for example.
In any case your argument wasn't C++ templates suck. We know. It was RTTI is better than CTTI, which is a bit bonkers.
when you do do things naïvely
Many problems are exponential if you're a bad enough coder, I don't see how that supports your point.
-5
u/gingerbill 1d ago
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.
9
u/QuaternionsRoll 1d ago
What? C++ has had RTTI in the form of virtual functions and abstract classes since forever
2
u/gingerbill 1d ago
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.7
u/QuaternionsRoll 1d ago
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.
0
u/gingerbill 1d ago
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 1d ago
>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 1d ago
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 (0)1
u/torsten_dev 1d ago
Rust has much better ergonomics around dynamic dispatch, but the object safety rules do make it painful from time to time. So does language design matter? Of course it does.
Rust let's you make the tradeoffs yourself but isn't famous for compiling fast, C++ requires some ugly inheritance, Odin has a runtime cost.
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/gingerbill 1d ago
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/torsten_dev 1d ago
Firstly, I didn't say Odin had dyn dispatch. The comparison was between rust and C++.
Secondly dyn dispatch is an option, so programmers can decide where and when they want to pay the cost.
Thirdly whatever insult you inferred you probably deserve.
2
u/gingerbill 1d ago
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.
5
u/torsten_dev 1d ago
I mean that's a joke against Perl users. I've yet to find a Perl user that gets offended at jokes against Perl.
Tastes differ. Yours clearly differs to mine, that's all that was meant to convey.
3
u/gingerbill 1d ago
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 (0)
1
u/levodelellis 11h ago edited 11h ago
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
1
u/LonelyAndroid11942 1d ago
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?
0
u/fireantik 1d ago edited 1d ago
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 1d ago
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.
0
u/fireantik 23h ago edited 23h ago
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.
1
u/gingerbill 23h ago
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-msvcis 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
36
u/SirClueless 1d ago
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::mapare 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 thestd::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 ofstd::map.At the end of the day,
mainis 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.