为我定制的编程语言
A programming language made for me

原始链接: https://zylinski.se/posts/a-programming-language-for-me/

Odin 融入了多项 C 语言最佳实践,这源于作者在 Our Machinery 的经验,在那里他们使用 C 语言开发了一个游戏引擎。Odin 内置了自定义内存分配器,允许使用超越 `malloc` 和 `free` 的高级内存分配策略,这与他们自定义的 C 语言内存分配器接口如出一辙。临时的内存分配器对于游戏开发至关重要,可以通过 `context.temp_allocator` 方便地访问,简化了短生命周期动态内存的管理。追踪分配器有助于检测内存泄漏,在程序关闭时打印未释放的内存分配。Odin 强调“零初始化”(ZII),自动将变量、结构体和其他数据结构清零初始化。类似于 C 语言的结构体指定初始化器允许选择性地初始化字段。最后,Odin 通过 `#soa` 指令内置支持数组结构体 (SoA),提高了缓存友好性。Odin 优先考虑简洁性,保留了 C 语言的优雅性,同时融入了泛型等现代特性。

Hacker News 的一篇帖子讨论了 Odin 编程语言,其创建者称赞其自动零初始化功能减少了未初始化变量的问题。D4ckard 认为,C 语言可以通过自定义分配器和精心设计的标准库来实现类似的结果,从而解决了 C 语言标准库过时的弱点。mrkeen 批评 Odin 的零初始化,认为尽早捕获未初始化值错误更好,即使会导致程序崩溃。他建议对读取未写入内存的行为进行编译时错误检查,类似于 Rust 的方法。D4ckard 反驳道,零初始化虽然不能防止错误的值,但至少避免了未定义行为 (UB)。Ratatoskrt 指出,编译时检查未写入内存是 Rust 的一个关键特性。这场讨论突出了不同编程语言设计选择中便利性、安全性以及调试之间的权衡。

原文

In my book Understanding the Odin Programming Language I wrote that “Odin incorporates some of my favorite C best practices, straight into the language”. But I didn’t really elaborate on the details. Let’s do that here!

This brings me to talking a bit about a previous job I had. Back in 2021 I worked at a place called Our Machinery. We were creating a whole game engine in plain C. We used a very comfortable and powerful way to program C.

We relied on concepts such as:

  • Custom allocators
  • Temporary allocators
  • Tracking allocators
  • Designated initializers
  • Zero is initialized
  • Cache friendly programming

While working there, I stumbled upon Odin. I read a bit about it. It seemed to incorporate all these things. In many ways it seemed like a language built around the specific way in which we programmed C at my job. Since I liked that way of programming, it almost seemed like a language built for me!

Custom allocators

At my job we had implemented our own Allocator interface in C. An allocator provides a custom way in which one can do dynamic memory allocations. C programmers are used to malloc and free. But you can make allocators that provide more advanced allocation strategies. Our Allocator interface made it possible to reason about allocators in a uniform way and pass them around to functions.

If a function accepted a parameter of type Allocator, then it was a hint that its return value was dynamically allocated.

This is exactly how it works in Odin. But the Allocator interface is built into the language’s base library collection! This meant that Odin’s base and core libraries support these allocators too. At my job, the Allocator interface was only supported in our own code: The C standard library didn’t support any of that. But in Odin, my own code and the core libraries can reason about custom allocators, making the concept even more powerful.

In Odin, core and base are two collections of libraries that come with the compiler. Some refer to them as the “standard library” of Odin. But they are shipped as source with the compiler. You are encouraged to make copies of the packages inside core, making it possible to tailor those packages to your own needs. So it’s more of a “default library” than a “standard library”. There’s a sensible default, but you’re free do things however you want.

Temporary allocators

Temporary memory allocators provide a way to do dynamic memory allocations that are only needed for a short while. What’s “a short while”? Video games have a very convenient “short while”: A single frame.

At my C job we had a temporary allocator that I used a lot. Gone was the need to manually malloc and free strings and arrays that were only needed for a short while. Just use the temp allocator. It’ll be gone the next frame! And it’s more efficient: The temp allocator allocates into pre-allocated blocks of memory.

I was happy to discover that Odin came with exactly this functionality. There’s a built in temp allocator available under the name context.temp_allocator. And again, the core libraries and my code uses the same Allocator interface. So I can just pass context.temp_allocator into any core library procedure that accepts an Allocator parameter. Whatever that procedure allocates will then be temporary. Nice!

Odin lets you choose when to clear the temp allocator. You do that by putting free_all(context.temp_allocator) somewhere in your code. In a video game, I’d put it as the last line of the “main game loop”.

Tracking allocators

Manual memory management may seem hard. How do you know if you’re leaking memory?

At my C job we had a special tracking allocator that could wrap any other allocator. It recorded when an allocation happened, and recorded when it was deallocated. That way we could display a warning on shutdown, if anything hadn’t been deallocated.

This is exactly how the tracking allocator that comes with Odin works. Just plop the code below at the top of your main procedure. It’ll print a list of memory leaks on shutdown.

track: mem.Tracking_Allocator
mem.tracking_allocator_init(&track, context.allocator)
context.allocator = mem.tracking_allocator(&track)

defer {
	if len(track.allocation_map) > 0 {
		fmt.eprintf("=== %v allocations not freed: ===\n", len(track.allocation_map))
		for _, entry in track.allocation_map {
			fmt.eprintf("- %v bytes @ %v\n", entry.size, entry.location)
		}
	}
	mem.tracking_allocator_destroy(&track)
}

You’ll also need to do import "core:mem" and import "core:fmt" at the top of the file. The code above is from the Odin overview.

Zero is initialized (ZII)

ZII, short for zero is initialized, means that you try to make the zero-value of memory valid in as many situations as possible.

In Odin all variables are automatically zero initialized. Not just integers and floats. But all structs as well. Their memory is filled with zeroes when those variables are created. So if Some_Type is a struct, then you can just write the following line to declare and zero-initialize a variable of that type:

This makes ZII extra powerful! There is little risk of variables accidentally being uninitialized. You can lean on that zero initialization. Also, the whole core library of Odin relies on ZII as well. So it feels very natural throughout the whole language and its ecosystem.

You can skip zero initialization by writing x: Some_Type = ---. You rarely need to do so, but it can be a good idea in some specific, performance sensitive situations. It’s great that zero initialization is opt out, instead of opt in. That way we get way less bugs due to uninitialized memory.

Designated initializers

This is a feature built into both C and Odin. The code below will create a variable x of type My_Type. It’ll initialize the field number to 7. Any non-mentioned field will be zero-initialized. This plays very well together with the “zero is initialized” concept.

My_Type :: struct {
	number: int,
	sub_thing: Another_Type,
}

Another_Type :: struct {
	some: int,
	more: f32,
	state: bool,
}

x := My_Type {
	number = 7,	
}

Cache friendly programming

The CPU has some memory inside it that is very fast. It’s called a cache. If you keep the cache filled with whatever data the CPU might need next, then your program will run very fast.

At my C job we had an entity-component-system (ECS) that used what is known as “Structure of Arrays” (SoA). That’s a memory layout that can, in certain circumstances, help fill your CPU cache with relevant data. Anyone who has written SoA data types in C knows it’s not very fun.

However, Odin comes with built in SoA support. Just put #soa in front of an array declaration. It’ll automatically re-arrange the memory layout for you.

As an example, the following code uses the “default layout”. Also known as “Arrays of Structures” (AoS):

Person :: struct {
	health: int,
	age: int,
}

people: [128]Person

The memory layout of the people array looks like this:

people[0].health
people[0].age
people[1].health
people[1].age
people[2].health
people[2].age
people[3].health
people[3].age
people[4].health
people[4].age
... etc

If you add #soa in front of [128]Person, like so:

Person :: struct {
	health: int,
	age: int,
}

people: #soa[128]Person

then the memory layout of people will instead look like this:

people[0].health
people[1].health
people[2].health
people[3].health
people[4].health
... and 123 more health items
people[0].age
people[1].age
people[2].age
people[3].age
people[4].age
... and 123 more age items

Achieving this in C is manual work. But here you just add #soa.

Now, don’t go putting #soa everywhere, just because you can. It will still make the code a bit trickier to write (you need to use #soa pointers etc instead of normal pointers). The code will also be a bit harder to debug. Put it in if you have proof of a clear performance benefit.

By the way, I discourage anyone who is making their own video game from making an ECS. It’s usually not a good idea. Perhaps it’s a good idea for some massive game engines. But for a small project it might just make your code harder to write and thereby your game worse. I feel like people who write an ECS before starting on their gameplay code are people who actually don’t want to make games: They want to make general purpose game engines. That’s fine, but make sure you’re not lying to yourself about what it is you want to do. If you want to make a game, then just make a game. Write the code that you need for the problem at hand, don’t pretend to be a giant game engine company.

Finally: Simplicity

Odin is very simple language. The reason my job used C over C++ was partially because of the simplicity of C. But we did sometimes miss some modern ideas of C++. However, C++ is a massive beast. We didn’t want to open that can of worms.

Odin retains the simplicity of C while bringing along some nice modern things, such as generics and (explicit) overloading. But the language is still kept small and simple. And it is meant to remain so. Very few language features have been added to Odin over the last few years. It’s mostly the core libraries that are getting major changes at this point.

Not everyone has my programming background

Learning Odin came naturally to me, due to it being so similar to the way in which I wrote C code.

You may come from a different background. If these things are unfamiliar to you, but you still want to learn Odin, then perhaps you’d benefit from reading my book Understanding the Odin Programming Language. It’s meant to be a reader-friendly intro to the language. The book tries to give you the insights I already had when I discovered Odin.

Thanks for reading!

Why not drop by my Discord server? On there you can ask me questions as well as discuss Odin and game development.

Have a nice day!

/Karl Zylinski

联系我们 contact @ memedata.com