Zig 0.14.0 released

https://ziglang.org/download/0.14.0/release-notes.html

41 Likes

There are few really good QoL changes made here. I am simple man, and easy to please.

Decl literals is actually kind of a game-changer for the language, and will allow for a lot of declarations to be more clear and concise, as well as more convenient.

Labelled switches is also a nice addition, and the move towards embracing “Unmanaged” as the default container type, I really hope the next version sees this applied to other containers such as ArrayList, etc.

9 Likes

At the bottom of the section, above the snippets it says

0.14.0 Release Notes ⚡ The Zig Programming Language
The other “managed” container variants are also deprecated, such as std.ArrayList.

4 Likes

Corresponding discussion on Hacker news has very few comments. Please say something :slight_smile:

Another more fresh Hacker News submission

Discussion on lobsters: Zig 0.14.0 Release Notes | Lobsters

I don’t understand the unmanaged change.
How does having to specify the allocator on every operation better than having it once during init?
Or maybe I’m just completely misinterpreting it?

While it does hold a few downsides (you have to manually pass allocators more often), it has a few upsides as well:

  • You know exactly what functions may allocate memory (this includes both the functions of ArrayList itself, so people prioritize limiting the allocating calls to ArrayList, but also the functions which take ArrayLists as a parameter - which some people use quite a bit)
  • Smaller ArrayList struct (reduces memory usage, especially if you have an ArrayList as part of some data that you hold in an array or something)
7 Likes

Passing the allocator as a parameter makes it immediately obvious what (and when) is allocating memory at a glance and saves 8 bytes for every managed instance.

5 Likes

I’ll just ask one thing. Is it type safety checked at compile time?
Let’s say I make a stupid mistake like I init the arraylist with gpa/debug allocator and used arena allocator to free it.
Do I get a runtime or a compile time error?

2 Likes

Unfortunately you will get run-time error. Allocator interface hides concrete allocator behind type erased pointer.

2 Likes

In practice these savings are immaterial. You are not going to use gazillions of empty or near-empty HashMaps or ArrayLists. After you fill them with data the extra overhead from the “header” struct is negligible. This is even more so with the latest push by Andrew towards data-driven design.

3 Likes

I think this will smell trouble for people who have to work with multiple containers with varying lifetimes. Skill issue, I guess but I’d rather have skill issue than my program blowing up.

Interesting points about the bytes saved. But if you’re doing hundreds to thousands of arraylists I think you have other problems rather than the bytes saved.

I’m not an expert, but my understanding is that having the allocator as a parameter, where it is const, is better for the optimizer than having it as a field, where it is var—you want the optimizer to be able to see that the function pointers in the VTable are never replaced by other function pointers, allowing the optimizer to devirtualize the call. i guess it’s probably significantly easier to prove this when the allocator is not a struct field.

1 Like

I deleted the post because the savings were 16 bytes because that is the size of std.mem.Allocator, not because of padding.

In practice these savings are immaterial. You are not going to use gazillions of empty or near-empty HashMaps or ArrayLists.

Not necessarily, consider VMs in which you have no idea how many array lists or hash maps the user is going to want.

pub fn ArrayList(comptime T: type) type {
  return struct {
    const Self = @This();

    allocator: std.mem.Allocator,
    _underlying_array_list: std.ArrayListUnmanaged(T),

    pub fn init(allocator: std.mem.Allocator) ArrayList(T) {
      return Self{
        .allocator = allocator,
        ._underlying_array_list = .{},
      };
    }

    pub fn deinit(self: *Self) void {
      self._underlying_array_list.deinit(self.allocator);
    }

    pub fn append(self: *Self, item: T) !void {
      self._underlying_array_list.append(self.allocator, item);
    }

    pub fn toOwnedSlice(self: *Self) !std.ArrayListUnmanaged(T).Slice {
      self._underlying_array_list.toOwnedSlice(self.allocator);
    }
  };
}

:slight_smile:

1 Like

Thanks. I’m just going to use this pattern in my own projects tbh.

I mean I think slonik is right in that most ArrayLists are likely to hold much more data that 16 bytes, making the savings comparably small, and I’m not sure where VMs come into the picture. Could you elaborate on that example.

This change does nicely fall in line with the goals and philosophy of the language though so the changes make sense. Many structs already hold an allocator so it’s not that big of a deal imo.

1 Like

in that most ArrayLists are likely to hold much more data that 16 bytes

This depends on what you are doing and I was mentioning a contradiction. Let’s say you are making an interpreter for a language that has objects. Then, you must have a hash map for each created object. If a program is creating a lot of empty objects for some reason, N, then you would save 16N bytes when using unmanaged over managed (24 bytes vs. 40 bytes on x86_64 ReleaseFast). We still use O(N) memory, but I’ll gladly take a reduction in object memory usage when it’s as easy as using unmanaged and passing allocators.

That’s a good use for this change as it applies to mappings. Are there similar ones for ArrayList? Genuinely curious, I can’t think of one.