Earlier quoted context omitted.
That is amazing, isn't it? I want to understand, in: uint64_t sum5(Node *node) { uint64_t value = 0; Node *next = NULL; for (; node; node = node->next) { for (;;) { value += node->value; if (node + 1 != node->next) { break; } node++; } } return value; } How is this magic: if (node + 1 != node->next) { break; } Better than the more intuitive: if (node->next == null) { break; } Also.. why is `node + 1' even a valid com…
> Also.. why is `node + 1' even a valid comparison here? In C and C++, when you add 1 to a pointer, you actually make it point to the next object of that size in memory, e.g. if I have a pointer to a 4-byte integer at address 0x8000, incrementing it will make it point to address 0x8004. Because arrays are contiguous chunks of memory, you can use this to iterate through arrays. int array[50]; int *p = &array[0]; // Ta…
Beating the L1 cache with value speculation (2021)
51–60 of 87 posts
Re: Beating the L1 cache with value speculation (2021)
#52Nice article! Incidentally, value speculation (or prediction) is a way to break causality in concurrent memory models.
Re: Beating the L1 cache with value speculation (2021)
#53Earlier quoted context omitted.
> Also.. why is `node + 1' even a valid comparison here? In C and C++, when you add 1 to a pointer, you actually make it point to the next object of that size in memory, e.g. if I have a pointer to a 4-byte integer at address 0x8000, incrementing it will make it point to address 0x8004. Because arrays are contiguous chunks of memory, you can use this to iterate through arrays. int array[50]; int *p = &array[0]; // Ta…
Makes sense, thanks! I'm still curious how this compiles down to faster ASM than a check for null.
Bottlenecks are introduced by dependencies, like if an instruction modifies one register and the next one immediately uses its value. With a pipeline, the result of the previous instruction might not be written back to the register in time for the current instruction to read the correct value from it, causing the CPU to execute the instruction incorrectly. So, you have to stall the pipeline and do nothing until that register is written to.
Memory loads cause bottlenecks for similar reasons. In fact, loading memory is usually the slowest instruction.
One way of getting around this is to execute instructions out of order, but do it in such a way that it looks like it's executing in-order. For dealing with branches, you can speculatively execute both branches, but only commit one result to the CPU state.
By testing whether the next node in memory was actually the next node in the list, the CPU could speculatively start executing the next iteration of the loop body as though it were; if it turns out not to be, then it can just not commit the results. If it is, then you've avoided a pipeline stall.
Note: I'm not a CPU expert; I just took a class in computer architecture recently. I could be mistaken about something, so feel free to correct me.
Re: Beating the L1 cache with value speculation (2021)
#54Earlier quoted context omitted.
Makes sense, thanks! I'm still curious how this compiles down to faster ASM than a check for null.
CPUs can execute arithmetic instructions fast, because we've been trying to execute sequential instructions faster for years. One way to do this is to create a pipeline: while the result of one instruction is being calculated, start reading the next; when the result is being written to a register, you're executing the previous one. Bottlenecks are introduced by dependencies, like if an instruction modifies one regist…
It seems unfortunate the compiler isn't able to realize "no further assignments are ever performed on node->next, it is side-effect dependency free, optimize the spec exec accordingly". Though, how often would this be the case in a real-world program of actual utility.. probably rare.
Re: Beating the L1 cache with value speculation (2021)
#55Earlier quoted context omitted.
CPUs can execute arithmetic instructions fast, because we've been trying to execute sequential instructions faster for years. One way to do this is to create a pipeline: while the result of one instruction is being calculated, start reading the next; when the result is being written to a register, you're executing the previous one. Bottlenecks are introduced by dependencies, like if an instruction modifies one regist…
This jives with my understanding of pipelining architecture and speculative execution, thank you for connecting the dots! Seriously cool to understand why that line works better. It seems unfortunate the compiler isn't able to realize "no further assignments are ever performed on node->next, it is side-effect dependency free, optimize the spec exec accordingly". Though, how often would this be the case in a real-worl…
Re: Beating the L1 cache with value speculation (2021)
#56It’s interesting to me that the final assembly-trick-free version almost no longer looks like a hack. If you commented the inner loop with something like “// Linear scan for adjacent nodes”, the reader gets an OK, if incomplete, intuition for why it’s faster. Even if you don’t know the exact CPU details, if you’re aware that flat arrays usually loop faster than contiguous linked lists, the nested loop immediately rea…
That is amazing, isn't it? I want to understand, in: uint64_t sum5(Node *node) { uint64_t value = 0; Node *next = NULL; for (; node; node = node->next) { for (;;) { value += node->value; if (node + 1 != node->next) { break; } node++; } } return value; } How is this magic: if (node + 1 != node->next) { break; } Better than the more intuitive: if (node->next == null) { break; } Also.. why is `node + 1' even a valid com…
Re: Beating the L1 cache with value speculation (2021)
#57Re: Beating the L1 cache with value speculation (2021)
#58This got me wondering - it's said that C is based on the lie that all computers have the same architecture as a PDP-11. (At least, I'm pretty sure I remember people saying that). So, are there any programming languages that have updated architectural models, something that takes into account branch prediction, CPU caches, etc?
I'm sure you could contrive a language where this functionality is exposed, but I'm struggling to come with an example where this would be seriously beneficial across multiple platforms.
I strongly suspect that integrating editors of existing languages with tooling that informs programmers on how a given chunk of code performs with parallel execution units would be far more beneficial than inventing a language dedicated to such concerns at this time.
Re: Beating the L1 cache with value speculation (2021)
#59There have been a lot of comments about the example presented being quite artificial and I agree but it is simple enough to help the reader understand what's happening and why it's faster.
In fact, it would be fairly common for the nodes in linked lists to be sequential in ram anyway. For example this code shows that the next node is easy to guess. The nodes do end up exactly in sequence in memory:
#include
#include
typedef struct Node {
int value;
struct Node *next;
} Node;
Node *head = NULL;
Node *tail = NULL;
int main(int argc, char **argv) {
// Allocate some nodes
for (int i = 0; i value = rand();
new_node->next = NULL;
if (tail == NULL) {
head = tail = new_node;
} else {
tail->next = new_node;
tail = new_node;
}
}
// Print their locations in memory
for (Node *current = head; current->next != NULL; current = current-> next) {
printf("%p\n", current);
}
}Re: Beating the L1 cache with value speculation (2021)
#60He's just published "Finding Simple Rewrite Rules for the JIT with Z3":
https://www.pypy.org/posts/2024/07/finding-simple-rewrite-ru...