SYCL 2023: ATTACK of the KILLER FEATURES by Martin Wickham

Martin will be in chat to answer any questions during the premier.


Anticipation is unbearable! Easily my largest question about Zig by a wide margin!

1 Like

That’s some awesome talk right there!

Leaving here those slide links that provide some explanation as to why, for instance, after RLS and PRO, pointers can be harder for the compiler to optimize than values:

  1. Understanding Compiler Optimization - Chandler Carruth - Opening Keynote Meeting C++ 2015 - YouTube
  2. LLVM Optimization Remarks - Ofek Shilon - CppCon 2022 - YouTube
  3. CppCon 2015: Chandler Carruth "Tuning C++: Benchmarks, and CPUs, and Compilers! Oh My!" - YouTube
1 Like

One thing I’ve been wondering here (which would probably make the situation worse, not better), is why we do “Parameter Reference Optimization”? To me, it seems that it rather should be the opposite, “Parameter Value Optimization”. Let me explain!

Looking at that for-loop example,

for (structs) |*s| {

, the * is such a giant footgun. Too easy to miss, and the cost of the miss is subtle implicit copy — usually not wrong, might be hard to notice in a profile but eats into your memory bandwidth!

From a different angle, a bunch of structs in Zig are unmovable/pinned. If I have something where the address is important, like MyFoo or BoundedArray(MyFoo), I would like to define methods like fn from(foo: MyFoo) and be assured that I am being passed in a reference to my pinnned MyFoo. It would be unfortunate if, when your struct is pined, to have to define all methods with * const.

These two together make me think that perhaps a more natural semantics for “by-value” function parametres and for / if captures is to be a read-only reference? Like, “I need a copy of data” is a super-rare case, the common case is “I want a read-only alias for this location”, and it would makes sense that the least noisy syntax to follow this default. Rust does this sorta-wrong, because you almost always pass arguments by &T and rarely by T. Val I feel does this right. Heck, we could go full Val even, and introduce a @copy builtin to mark places where you don’t want an alias (const x = foo.x aliases, const x = @copy(foo.x) makes a copy).

A world where fn foo(x: T) or for(xs) |x| means “pass by constant reference” feels somewhat more natural to me. In that world, we would need an optimization that, for small types whose addresses can’t matter, would do a copy instead.


It’s interesting how the List.add example looks in that world. You write

fn add(self: *List, item: T) void {
    self.items.len += 1;
    self.items[self.items.len-1] = @copy(item);

and it sort-of looks ugly — there’s this @copy here, surely we don’t need that here? Which pushes it to the following API instead:

fn add(self: *List) *T {
    self.items.len += 1;
    return &self.items[self.item.len - 1];

which changes the callsite from xs.add(item) to xs.add().* = @copy(item), and makes “the bug” impossible (granted, we are exposing undefined value here, but that feels maybe OK in Zig, and certainly easier to safety-check).

This also reminds me discussion about placement in Rust. Rust has trouble with placement, because it changes evaluation order. In Rust, if you do Box::new(f()) or list.push(f()), f() is evaluated first, and then Box/Vec code runs to allocate the memory. That’s not optimal, what you want is to first allocate the memory, and then construct the value in place there. And, given that Zig is for explicit control flow, we maybe want to make this “allocate, then emplace” pattern the path of least resistance in stdbli/language?

(Note: not a language designer, level of confidence for all of the above is like 0.3)


Or maybe not, the trouble with that sort of an API is that it’s not exception-safe:

 xs.add().* = try f(); // BUG, could leave `xs` with a wrong length. 

What you really want is rather

xs.willAdd().* = try f();

but it would be a shame if every collection operation is two lines…


Sorry, I’m not sure I understand your note here on the fact that “by-pointer capture” syntax introduces a copy. I think that Martin mentions that it’s the opposite, and he wrote it like that because it doesn’t introduce a copy in the loop, here’s the timestamp:

I’m pretty sure that he just meant that it’s easy to miss the *, and when you miss it, it introduces a copy (which is what you don’t want in 99% of the cases), so what he is calling for is catch(and pass) by const reference by default instead.


Yup, “missing * in a for loop” is a particular footgun we like shoot like once a week at TigerBeetle.


Oh, yeah, totally! Also, now that you’ve both brought it up, I see why pass/capture by constant pointer should totally be the default.


I definitely like the idea of having a const ref by default. Especially since I miss mut from Rust. I find it very powerful to be able to define when a parameter is going to be changed. This is most of the time relevant for member functions but for these I find it essential.

1 Like

This was a really interesting talk that I think anyone interested in Zig, Jai… etc… should watch.