忘记借用跳棋:C3解决了使用范围的记忆寿命
C3 solved memory lifetimes with scopes

原始链接: https://c3-lang.org/blog/forget-borrow-checkers-c3-solved-memory-lifetimes-with-scopes/

C3的新临时分配器通过将垃圾收集的易用性与手动管理的控制结合在一起,简化了动态内存管理。它利用内存分配区域(竞技场),在退出时自动重置的范围内分配内存,从而防止内存泄漏。这种方法通过当地提高性能,因为分配聚集在一起,从而增强了CPU访问。 通过“ @pool()”定义分配变量的范围。通过将当前范围的分配器(`TMEM')分配给变量,可以明确控制清理。在许多情况下,可以省略“ @pool()”声明,因为在检测到临时分配时,编译器会自动将其添加到`main()'函数。 该系统在性能和易用性之间提供了平衡,消除了对RAII,参考计数或垃圾收集的需求。 C3的临时分配器提供了近乎无效的内存管理,非常适合低延迟系统和不喜欢垃圾收集开销的显式控制的开发人员。

黑客新闻线程讨论了C3使用范围内的内存池(`@pool')的内存管理方法,并说明了它“解决内存生命的”。评论者在很大程度上批评了该文章的标题,因为它误导了将C3的方法与Rust的借贷检查员进行比较。共识是,C3的方法虽然有可能简化临时内存管理,但并没有提供与RUST相同的记忆安全性,尤其是与混叠和无用的漏洞有关。 专家强调,竞技场的词汇范围不是新的,类似于C ++的RAII或其他语言的实现。 人们对数据寿命超出池范围,缺乏编译时间检查以及潜在的运行时问题的情况引起了人们的担忧。 讨论还涉及C3中的合同,并指出缺乏保证的静态检查以及不确定行为的潜力。 尽管C3的系统旨在在类似于C的环境中易于临时内存管理,但其与借用检查的比较被认为是不准确的,并且其“解决方案”被认为是简化的,而不是完全解决内存安全问题。
相关文章

原文

Modern languages offer a variety of techniques to help with dynamic memory management, each one a different tradeoff in terms of performance, control and complexity. In this post we’ll look at an old idea, memory allocation regions or arenas, implemented via the C3 Temp allocator, which is the new default for C3.

The Temp allocator combines the ease of use of garbage collection with C3’s unique features to give a simple and (semi)-automated solution within a manual memory management language. The Temp allocator helps you avoid memory leaks, improve performance, and simplify code compared to traditional approaches.

Memory allocations come in two broad types stack allocations which are compact, efficient and automatic and heap allocations which are much larger and have customisable organisation. Custom organisation allows both innovation and footguns in equal measure, let’s explore those.

Memory Leaks

When we dynamically allocate memory, with say malloc() typically we need to free() it afterwards, otherwise we can’t use that memory until the OS process exits. If that memory isn’t being used and it has not been freed this is called a memory leak and can lead to restricting or running out of available system memory.

Avoiding Memory Leaks

Common solutions are RAII, reference counting or garbage collection which automatically free those variables.

Each method has different tradeoffs.

  • RAII needs classes or structs to manage their own memory cleanup with a lot of extra code.
  • Reference counting counts how many users each memory allocation has, when this hits zero the memory is freed. Reference counting is expensive with multiple CPU cores as we need to synchronise these counts and share information between cores.
  • Garbage collection competes with the program for both memory and CPU time, reducing program performance and increasing memory usage.

Memory Allocation Regions

Memory allocation regions, go by various names: arenas, pools, contexts; The idea dates back to the 1960’s with the IBM OS/360 mainframes having a similar system. Memory regions are efficient for managing many memory allocations and can be freed in a single operation, and are particularly effective when we know the memory will not be needed later. That is, we know the memory’s lifetime. This idea is powerful and used in many applications like web server request handlers or database transactions, as used by the Apache webserver and the Postgres database.

Memory allocation regions use a single buffer so have good locality because all the allocations are closely associated together, making it more efficient for CPU access, compared to traditional malloc where allocations are spread throughout the heap.

Memory allocation regions may make it easier to manage memory, however you still need to remember to free them, and if you forget to do call free, then that memory will still leak.

Enter The Temp Allocator

The Temp allocator in C3 is a region based allocator which is automatically reset once execution leaves its scope, so you cannot forget to free the memory and can’t leak memory. The Temp allocator in C3 is a builtin in the standard library, called @pool() and using it you can define the scope where the allocated variables are available, for example:

fn int example(int input)

// Allocate temp_variable on the heap with Temp allocator

int* temp_variable = mem::tnew(int);

input = input + temp_variable;

}; // Automatically cleanup temp_variable

assert(result == 57, "The result should be 57");

Check With Valgrind

Valgrind is a tool which detects memory leaks and we can use it to show the temp allocator managed the memory for us automatically.

valgrind ./pool_example |& grep "All heap blocks were freed"

==129129== All heap blocks were freed -- no leaks are possible

Success!

We have relatively performant memory allocations managed automatically without needing RAII, garbage collection or reference counting.

Controlling Variable Cleanup

Normally temp allocated variables are cleaned up at the end of the closest @pool scope, but what if you have nested @pool and want explicit control over when it’s cleaned up? Simple: assign the temp allocator with the scope you need to a variable, and use it explicitly. The temp allocator in a scope is a global variable called tmem.

fn String* example(int input)

// previous global temp allocator from main() scope

Allocator temp_allocator = tmem;

// Allocate on the global temp allocator

String* returned = allocator::new(temp_allocator, String);

*returned = string::format(temp_allocator, "modified %s", input);

// top-most temp allocator, tmem created here

String* returned = example(42);

// "modified 42" string returned

A Handy Shorthand

We can reduce the code’s nesting using short function declaration syntax => making it even simpler as:

fn int example(int input) => @pool(reserve: 2048)

// Allocate temp_variable on the heap

int* temp_variable = mem::tnew(int);

input = input + *temp_variable;

In Simple Cases Omit @pool()

Happy with the defaults? We can actually omit the @pool() in main() all together!

The compiler automatically adds a @pool() scope to the main() function for us, once it finds a temp allocation function like mem::tnew(), without an enclosing @pool() scope. That simplifies our code to:

fn int example(int input)

// Allocate temp_variable on the heap

// @pool() temp allocator created for us by the compiler

int* temp_variable = mem::tnew(int);

input = input + *temp_variable;

Conclusion

The Temp allocator has landed in C3; combining scoped compile time known memory lifetimes, ease of use and performance. Tying memory lifetimes to scope and automating cleanup, you get the benefits of manual control without the risks. No more memory leaks, use after free, or slow compile times with complex ownership tracking.

Whether you’re building low-latency systems or just want more explicit code without garbage collection overhead, the C3 Temp allocator is a powerful tool to have in your arsenal, making memory management nearly effortless.

Want To Dive Into C3?

Check out the documentation or download it and try it out.

Have questions? Come and chat with us on Discord.

联系我们 contact @ memedata.com