38 Comments
User's avatar
⭠ Return to thread
Vjekoslav's avatar

This is not how this scratch system works. If you call GetScratch twice in a row, both for temp allocations (you don't pass first scratch as a conflict to the other), you will get same arena for both calls. And that is fine, this is how stack works. Even in case when arenas are in conflict, fixed counter would disable alternating same arenas in arbitrarily deep stack as Ryan mentioned with his example.

Expand full comment
Trần Thành Long's avatar

So I got a notification about your reply about why did I want that, but I can't find it, so maybe you deleted your reply? I will explain my idea a little more so that we are all on the same page.

The OP was talking specifically about the per-thread scratch system and was not about the stack-like arena fashion. In the article, Ryan introduced the GetScratch function that takes in an array of Arena* and returns a unique arena to the array from the thread pool (technically, it returns a TempArena that reference the unique arena). The original array of arenas can only contain a thread-local scratch arena when and only when that scratch arena was from the thread-local scratch pool by calling GetScratch. My idea was rather than pass in a not-to-conflicts-with array to the GetScratch, it just has a counter that gets increased every time it gets called and returns the element at that index from the pool. Then when the current function is done using the scratch arena, it will call the ReleaseScratch function which will decrease the counter by one so the latter functions can reuse the arena.

Expand full comment
Vjekoslav's avatar

I don't know if by OP you meant your original reply or Ryan's post. But Ryan introduced scratch system in the context of stacks.

As for your implementation, yes that would work too, but then you'd have to dynamically increase arena pool when your memory stack grows beyond pool capacity. (Or allocate large enough pool at start by measuring your code, which could be a bit tricky if you use recursions a lot). I think Ryan's approach is more elegant exactly because of arena re-usage. You don't have to measure your code, you just have to follow simple rule Ryan outlined. As long as only a single persistent arena is present at any point in any codepath, you will not need more than two scratch arenas.

Expand full comment
Trần Thành Long's avatar

I meant my original reply.

So it seems like you misunderstand me. You can still have a persistent arena, and all your functions still need to pass in an arena like the function A and function B example. I was talking specifically about the GetScratch and ReleaseScratch functions that were used inside functions A and B.

void *FunctionA(Arena *arena)

{

ArenaTemp scratch = GetScratch(); // increase the counter

// use the scratch arena

// fill result to Arena* arena

ReleaseScratch(scratch); // decrease the counter

return result;

}

void FunctionB(void)

{

ArenaTemp scratch = GetScratch(); // increase the counter

void *result = FunctionA(scratch.arena); // the scratch inside A will not be the same as this scratch

// use result

ReleaseScratch(scratch); // decrease the counter

}

Expand full comment
Trần Thành Long's avatar

I can't find a way to turn on the code format in the reply section, so here's a link with some example code about the difference between Ryan's way and mine: https://pastebin.com/h53XsVzi

Expand full comment
Eiríkr's avatar

Consider this usage code:

ArenaTemp a = GetScratch();

ArenaTemp b = GetScratch();

ReleaseScratch(a);

ArenaTemp c = GetScratch();

In your approach, `b` and `c` would be the same scratch arena.

Expand full comment
Trần Thành Long's avatar

Scratch arena should work in a stack-like fashion, which means there can't be overlapped lifetime in the current scope. Either c is initialized before releasing a, or you release b before initializing c.

Expand full comment
Eiríkr's avatar

Does it? The article never states it explicitly. I imagine it would've been mentioned as another rule.

Expand full comment
Trần Thành Long's avatar

To be more precise, the article didn't say anything specific about this. This is just an implementation of mine, which means it isn't the same as the original approach. It has its own pros and cons and isn't 100% better than the original one. You just need to see if the advantages outweight the disadvantages. Do you really need to create c before releasing b or release a before creating c?

Expand full comment
Eiríkr's avatar

Ah, I see. I haven't used arenas that much and haven't used scratch arenas at all, so I cannot comment on that. Not yet, at least. But what I can say is: I don't really like that approach because it introduces an additional rule but gives little in return (the performance impact is negligible, and the code might get more complicated because of the rule).

Expand full comment
Trần Thành Long's avatar

The code actually gets less complicated because you don't need to remember passing in all the previous scratches.

// Your way

ArenaTemp a = GetScratch();

ArenaTemp b = GetScratch(a);

ArenaTemp c = GetScratch(a, b);

// My way

ArenaTemp a = GetScratch();

ArenaTemp b = GetScratch();

ArenaTemp c = GetScratch();

Expand full comment
Trần Thành Long's avatar

You can't get the same arena until the counter goes down. The GetScratch method only gets the scratch at the counter and increments it by one. Kind of like this: #define GetScratch(name) ArenaTemp name = scratchPool[scratchCount++] and #define ReleaseScratch(name) ArenaTempEnd(name); scratchCount--

Expand full comment