News for hackers. Stuff that matters.
Re:Six times faster than C by torstenvl on Thursday July 06, 2023 @04:56PM (#36622891)
I'm not so sure that the right take-away is "hand-written assembler is 6x faster than C." It's more like "jumps are a lot slower than conditional arithmetic." And that can [edit:often] be achieved easily in C by simply not using switch statements when an if statement or two will work fine.

Rewriting the C function as follows got a 5.5x speedup:

    int run_switches(char *input) {
        int r = 0;
        char c; 
        while (1) {
            c = *input++;
            if (c == 's') r++;
            if (c == 'p') r--;
            if (c == '\0') break;
        }
        return r;
    }
Results:

    [16:50:14 user@boxer ~/looptest] $ cc -O3 bench.c loop1.c -o lone
    [16:50:37 user@boxer ~/looptest] $ cc -O3 bench.c loop2.c -o ltwo
    [16:50:47 user@boxer ~/looptest] $ time ./lone 1000 1
    449000
    ./lone 1000 1  3.58s user 0.00s system 99% cpu 3.589 total
    [16:50:57 user@boxer ~/looptest] $ time ./ltwo 1000 1
    449000
    ./ltwo 1000 1  0.65s user 0.00s system 99% cpu 0.658 total
Re:Six times faster than C by BoppreH on Thursday July 06, 2023 @05:15PM (#36623180)
What version of GCC are you using? For me both versions perform the same, both on Ubuntu and Windows:

    $ time ./lone 1000 1
        851000

        real    0m3.578s
        user    0m3.574s
        sys     0m0.004s
        
    $ time ./ltwo 1000 1
        851000

        real    0m3.583s
        user    0m3.583s
        sys     0m0.000s

    $ gcc --version
        gcc (Ubuntu 9.4.0-1ubuntu1~20.04.1) 9.4.0
        Copyright (C) 2019 Free Software Foundation, Inc.
        This is free software; see the source for copying conditions.  There is NO
        warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
Re:Six times faster than C by torstenvl on Thursday July 06, 2023 @05:24PM (#36623280)
Sorry, I write 'gcc' purely out of force of habit. I'm using Clang/LLVM.

    [17:23:00 user@boxer ~/looptest] $ uname -a
    Darwin boxer.local 21.6.0 Darwin Kernel Version 21.6.0: Thu Jun  8 23:57:12 PDT 2023; root:xnu-8020.240.18.701.6~1/RELEASE_X86_64 x86_64
    [17:23:47 user@boxer ~/looptest] $ cc -v
    Apple clang version 14.0.0 (clang-1400.0.29.202)
    Target: x86_64-apple-darwin21.6.0
    Thread model: posix
    InstalledDir: /Library/Developer/CommandLineTools/usr/bin
Clang generates the sete instruction for me with the above code:

    [17:23:49 user@boxer ~/looptest] $ gcc -c -O3 loop2.c   
    [17:25:00 user@boxer ~/looptest] $ objdump -d --symbolize-operands --x86-asm-syntax=intel --no-show-raw-insn loop2.o
    
    loop2.o: file format mach-o 64-bit x86-64
    
    Disassembly of section __TEXT,__text:
    
    0000000000000000 <_run_switches>:
           0:       push rbp
           1:       mov rbp, rsp
           4:       xor eax, eax
           6:       nop word ptr cs:[rax + rax]
    <L0>:
          10:       movzx ecx, byte ptr [rdi]
          13:       add rdi, 1
          17:       xor edx, edx
          19:       cmp cl, 115
          1c:       sete dl
          1f:       add eax, edx
          21:       xor edx, edx
          23:       cmp cl, 112
          26:       sete dl
          29:       sub eax, edx
          2b:       test cl, cl
          2d:       jne  <L0>
          2f:       pop rbp
          30:       ret
Re:Six times faster than C by DeathArrow on Thursday July 06, 2023 @05:24PM (#36623284)
Shouldn't the compiler be able to do that, too?
Re:Six times faster than C by ModernMech on Thursday July 06, 2023 @05:55PM (#36623649)
Yes, there’s always the “sufficiently smart compiler” that can generate this code. Question is, does that compiler exist?
Re:Six times faster than C by Groxx on Thursday July 06, 2023 @06:01PM (#36623741)
I sure hope so. The semantics are trivially identical, the optimizations should be as well, by default - they should depend on semantics, not syntax. And GCC in another comment under this thread seems to be doing similar or identical optimizations in both cases.

I wholly admit that this implies nothing about all optimizers. But it's a pretty reasonable one to expect.

Re:Six times faster than C by charcircuit on Thursday July 06, 2023 @06:06PM (#36623806)
>does that compiler exist?

and if so are the compile times worth it

Re:Six times faster than C by icsa on Thursday July 06, 2023 @08:07PM (#36625132)
These days, I's put my money on zig cc. I.E. zig cc -Os or zig cc -O3
Re:Six times faster than C by eyegor on Thursday July 06, 2023 @08:21PM (#36625263)
Does the zig compiler have many fancy bits? I was under the impression the c support was a "nothing special" compiler that punted to llvm for optimizations.
Re:Six times faster than C by plorkyeran on Thursday July 06, 2023 @11:01PM (#36626543)
Correct, zig currently does not do any of its own optimizations. It won't necessarily have identical results to clang because it'll generate equivalent but not identical IR and the optimizer passes may happen to do something different as a result, but it's not going to be consistently different.
Re:Six times faster than C by thomasahle on Thursday July 06, 2023 @06:21PM (#36624026)
I tried https://godbolt.org/, and neither Clang nor GCC trunk give the same code for the two programs.

Pretty shocking for such a simple program.

Re:Six times faster than C by AnotherGoodName on Thursday July 06, 2023 @07:05PM (#36624525)
It's not true in general.

In general branching code is faster than branchless code and there's many many places that will demonstrate this with a quick Google. You know how many cycles a correctly predicted branch takes? 0.

On the other hand branchless code has to wait for each calculation to reach a certain stage in the pipeline since the thing to be output is dependent on the result. The CPU will have a whole lot of halts.

So why is this faster? Because the input is literally random(). The branch predictor will be wrong. This isn't normal though. The compiler is creating code that will be faster in most normal use cases.

It's an artificial benchmark that works against the output the compiler produces.

Re:Six times faster than C by rendaw on Friday July 07, 2023 @01:41AM (#36627650)
That's what profile guided optimizers are for though, right?
Re:Six times faster than C by haberman on Thursday July 06, 2023 @05:59PM (#36623708)
> jumps are a lot slower than conditional arithmetic.

This statement is true if the jumps are unpredictable. If the jumps are predictable, then jumps will be faster.

Linus had a whole rant about this back in the day, arguing that cmov is not useful if branches are predictable: https://yarchive.net/comp/linux/cmov.html

Re:Six times faster than C by torstenvl on Thursday July 06, 2023 @06:21PM (#36624031)
I haven't run any benchmarks, but jump-if-equal and set-if-equal would seem to have the same level of predictability.

My naive, untested intuition is that there's only one meaningful difference: the former has to dump the entire pipeline on a miss, and the latter only has to nop a single instruction on a miss.

But maybe I'm missing something. I'll re-read his rant.

Re:Six times faster than C by haberman on Thursday July 06, 2023 @06:41PM (#36624254)
> jump-if-equal and set-if-equal would seem to have the same level of predictability.

The difference is that branches have dedicated hardware (branch predictors) that will speculatively execute subsequent instructions based on their best guess about which way the branch will go. Whereas conditional moves cannot execute any subsequent instructions until the correct value is available.

Put another way, CPUs have control flow speculation, but not conditional move speculation. I don't know if conditional move speculation would be a feasible thing to implement or not, but I'm pretty sure that no mainstream CPUs have such a feature.

Re:Six times faster than C by Lk7Of3vfJS2n on Thursday July 06, 2023 @08:03PM (#36625103)
I'd be curious to learn why CPUs don't have conditional move speculation.
Re:Six times faster than C by Tuna-Fish on Thursday July 06, 2023 @08:32PM (#36625358)
Because modern CPUs as a rule don't speculate on values to arithmetic, only on control flow, and CMOV acts like arithmetic.

That is, if there is an add instruction on rax and rbx, no matter what, the add instruction will not execute until both rbx and rbx are available. If the result went into rax, and there is an another instruction that uses that as a source, no matter what that instruction will not execute until the add has completed.

CMOV is implemented as an ALU instruction that always writes into it's output, and either writes the value that is already in there (which is why it depends on the value of it's output) or the value provided, depending on flags.

Re:Six times faster than C by sitkack on Thursday July 06, 2023 @10:24PM (#36626255)
I hope you work on compiler backends.
Re:Six times faster than C by epcoa on Thursday July 06, 2023 @10:48PM (#36626452)
With all due respect this is quite literally the level of stuff covered in an undergrad EE architecture course and is covered in an elementary text like Patterson and Hennessy.
Re:Six times faster than C by sitkack on Saturday July 08, 2023 @04:36PM (#36648471)
> With all due respect

> quite literally

You could have conveyed the close to the same thing by saying, "things like this are covered in Patterson and Hennessy"

> elementary text

Jesus, do you even lift? The rest of the discussion is amazing.

Re:Six times faster than C by epcoa on Sunday July 09, 2023 @01:50AM (#36651933)
For those not aware Patterson and Hennessy is elementary (“relating to the basic elements of a subject.”) because it is often used in an introductory course of computer architecture. This isn’t a slight.
Re:Six times faster than C by torstenvl on Thursday July 06, 2023 @10:44PM (#36626418)
I'm not saying you're wrong — I'm completely ignorant at the microcode level — but it seems to me like between

    cmp x, y
    je z
and

    cmp x, y
    sete z
the actual speculative part is the same: speculating as to the result of cmp x, y

If that's true, why would it not simply pipeline sete and the following instructions and simply execute (or not execute) sete according to its prediction, and then double check itself and reverse (or apply) the operation of the prediction was wrong?

I probably just have a bad mental model of what's going on under the (under the) hood, so whatever patience you have to deal with my stupid questions would be greatly appreciated.

Re:Six times faster than C by celrod on Thursday July 06, 2023 @11:44PM (#36626836)
Taking the example:

      cmpb $115, %cl
      sete %dl
      addl %edx, %eax
vs

      cmpb $115, %cl
      jne _run_switches_jmptgt1
      mov $1,   %dl
     _run_switches_jmptgt1:  
      addl %edx, %eax
The argument about why `jne` might be faster is that that in the former case, the CPU always executes a dependency chain of length 3: `cmpb` -> `sete` -> `addl`. Each of these instructions have to be computed one after the other, as `sete` depends on the result of `cmpb`, and `addl` depends on the result of `sete`.

With `jne`, the CPU might predict the branch is not taken, in which case, the dependency chain is `mov` -> `addl` (the `mov` of an immediate might be handled by register renaming?).

Or that it is taken, in which case in which case the dependency chain is just `addl`.

I guess you're arguing that the CPU should handle `sete` the same way? That is, instead of treating `addl` as dependent on the result, predict what `sete` does and start executing `addl` before `sete` finishes, rewinding if that went wrong?

Re:Six times faster than C by torstenvl on Friday July 07, 2023 @12:38AM (#36627212)
Yeah, or at least I don't understand why that wouldn't be possible.

Microcode can set the EIP register based on its prediction of what the result of cmpb $115, %cl will be.

Why can't it set the EDX register based on its prediction of what the result of cmpb $115, %cl will be?

Re:Six times faster than C by gpderetta on Friday July 07, 2023 @02:35AM (#36628002)
In principle is perfectly possible to speculatively execute cmov (and viceversa to change jump-over-one-instruction into conditional execution).

But Intel historically didn't do it as programs tend to use cmov when the condition is unpredictable , so there was little reason to optimize it.

After Spectre, I believe intel has given an architectural guarantee that cmov is never speculated so it can be used as part of speculation attack prevention.

Re:Six times faster than C by ithkuil on Friday July 07, 2023 @04:50AM (#36629029)
The purpose of control flow speculation is to avoid stalling the pipeline.

If each instruction was executed in one single clock cycle, the cost of executing a branch would be one cycle and that's it.

However since there is a maximum speed at which operations can happen in hardware, the period of such a clock cycle that can execute a whole instruction would be very long and so the amount of "instructions per second" the CPU could execute would be low.

Now, if you can break up each instruction in smaller steps and execute the smaller steps in an overlapping manner, such that while you're executing the second step of the first instruction you're executing the first step of the next instruction and so on (like on an assembly line in a factory) you can have a much shorter clock period for each of these steps, and at the end of each clock tick an instruction would complete execution. The CPU will be still running one instruction per clock cycle, but since each clock period is shorter the overall instruction per second rate will be higher.

But for this to work the next instruction you want to execute must be known in advance so that at each clock cycle the CPU can start step 1 of a new instruction.

That's easy when the program is executing sequentially but when there are branches involved it's more tricky.

And that's tricky also if the branch is not conditional! If the instruction execution is broken into many small steps, it may take one or more steps before figuring out that you have a branch in the first place, let alone decoding where you need to branch to. In the meantime the CPU will have happily started to execute the first "steps" of the next instruction.

This is called a "branch hazard"

Early CPU implementations handled branch hazards by just throwing away the intermediate states if the few instructions that we're half way through the pipeline and call it a day (stalling the pipeline).

Early RISC CPUs attempted to be clever and use a trick called "delay slots": the instruction(s) already in the pipeline will continue to execute as if they were logically before the branch. This puta the onus to the programmer (or the compiler) to make sure that only instructions that are safe to be executed regardless of whether the branch is taken or not, are actually put after the branch instruction (otherwise you can just write nops).

But branch delay slots are not a panacea. As pipelines got deeper it became I practical to have a large number of delay slots and even a small number of delay slots were often just filled with nops anyway.

Improving on UNconditional branches was done by "looking ahead" in the instruction stream for branch instructions. When the instructions are all of the same size it's easy to quickly look a few instructions ahead and tell when you found a branch. You also need an instruction encoding scheme that is relatively fast to decode, at the very least it should be fast to decode branches (the more complicated the logic to decode a branch is, the farther ahead you'd have to look in the instruction stream, which in turn would limit the size of the sequence of instructions you can fill your pipeline with between subsequent branches).

To further complicate the matter, even if you found the branch instruction and you decoded it, it doesn't mean you yet know where it will branch to!

Indirect jumps (where the address is in a register) are similar to conditional jumps in that you don't know the address you're jumping to by merely looking ahead in the instruction stream and noticing the branch instruction. You need to either wait until you execute the branch and stall the pipeline in the meantime, or keep them in the pipeline and flush the pipeline once you know the target of the branch.

The next trick that CPU designers came up way before speculative execution is "branch target prediction".

The CPU keeps a little associative memory that maps addresses of a branch instruction to branch targets. When the lookahead logic spots a branch instruction it looks in this map and gets a guess of the branch target and uses that immediately ad the next instruction so that the pipeline is kept fed with something.

If by the time the branch instruction is executed the guess turned out to be wrong, the pipeline is flushed in the same way it would have to be flushed anyway if we had no clever branch lookahead in the first place. But if the guess was right we paid only one cycle to execute the branch.

This works for indirect unconditional branches and also for conditional branches! The prediction logic can be more subtle and complicated, many many things gave been attempted but this the general idea.

Re:Six times faster than C by Tuna-Fish on Friday July 07, 2023 @06:59AM (#36629901)
The two sequences look very similar, and could be implemented the same way, but the actual implementation could not be more different.

> If that's true, why would it not simply pipeline sete and the following instructions and simply execute (or not execute) sete according to its prediction, and then double check itself and reverse (or apply) the operation if the prediction was wrong?

You cannot just reverse or apply one operation. The way speculation works, when the frontend encounters a conditional jump, the entire architectural state of the current thread is stored, and all future memory writes are held in the store buffer and not written out. Then a long time, potentially dozens of cycles later, after the je is executed in the backend either the old state is restored and the pending writes are discarded, or the saved state is discarded and the pending writes are released.

In contrast, in ALUs, the inputs for instructions are always available before the instructions are scheduled to execute. It would be possible to implement sete like je, but this would imply significant changes to how and where it is executed. ALU ops cannot trigger speculation because there is no machinery for storing state at that part of the pipeline.

And no-one is ever going to implement cmov or sete like a jump, because moving the op from being an ALU op to being one that is speculatively executed in the frontend like jmp would make both positive and negative changes, and that would be a significant pessimization of existing software because for decades cmovs have been used for unpredictable values, where sequencing and waiting for the real value is a better idea than speculating and failing half the time. Using a cmov serializes execution when any following operations use the value, but if you can have independent work after it, you can always successfully execute that. Speculating at an unpredictable CMOV would cause that to be thrown away uselessly half the time.

Re:Six times faster than C by nwallin on Thursday July 06, 2023 @11:50PM (#36626867)
Speculative execution is all about control flow. It's about what value is in the instruction pointer at some nebulous point in the future.

A conditional jump can put one of two values into the instruction pointer, they will either increment the instruction pointer (jump not taken) or put the immediate value into the instruction pointer. (jump taken)

cmov/sete are utterly deterministic; they always increment the instruction pointer. There's nothing to speculate on, there's nothing to predict. They just go to the next instruction.

Re:Six times faster than C by kazinator on Friday July 07, 2023 @12:19AM (#36627049)
> Speculative execution is all about control flow. It's about what value is in the instruction pointer at some nebulous point in the future.

.. and all about what we can wheedle out of all the background speculation that will help us get root on this box.

Re:Six times faster than C by sweetjuly on Friday July 07, 2023 @04:14AM (#36628754)
> Speculative execution is all about control flow

It's murkier than that. Speculation also deals with the order in which instructions can be executed. Take for example memory ordering (discussed in a mini essay elsewhere here): we typically speculate that all loads are unrelated to any other older in-flight stores with unresolved addresses so that we can optimistically launch them. This is not a control flow issue but it is something we both speculate and predict (memory dependence predictors!) despite the next PC being essentially deterministic.

Re:Six times faster than C by sweetjuly on Friday July 07, 2023 @04:07AM (#36628694)
One other perspective is that by speculating the outcomes of conditional instructions, you naturally open yourself up to mispeculating them. This sounds obvious but the consequences for the uarch are quite severe. This is because anytime you mispeculate an instruction, most (all?) contemporary CPUs throw out all younger speculative progress (even if it is unrelated!) and restart at the instruction it originally mispeculated. Throwing out all this work is both i) a waste of power/cycles (you did all this speculative work for nothing!) and ii) quite an expensive operation because you either have to iteratively rollback the state (slow!) or take a snapshot the state on every conditional instruction (expensive from power/area perspective).

A similar idea to what you're proposing (and a possible solution to the above issue) does come up in another part of the processor however! Specifically, high performance processors launch loads very aggressively and often times return data as soon as the address is known. This is because memory is often the bottleneck for performance. This, unfortunately, has some challenges. Namely, memory ordering violations. Take for example the following snippet (ARMv8):

    mov x1, #1    
    udiv x3, x2, x1
    str x2, [x3]
    ldr x4, [x2]
    add x5, x4, x4
This is a silly and somewhat contrived code sequence, but note here that both str x2 and ldr x4 access the same address and thus the value in x4 should be x2. Note, however, that since str x2's address (x3) is produced by a slow division operation but ldr x4's address (x2) is available much more quickly, ldr x4 likely will launch before the CPU even knows that str x2 conflicts with it. Thus, the data returned by the load will be whatever random old stale data is in the cache rather than the correct value that is currently sitting in x2. This means that the subsequent add which consumes this data will produce an incorrect value, leading the whole program to derail. Once the CPU detects this issue, it has to throw away all the state and restart execution of the program at ldr x4 in order to fix its mistake and fix up the memory ordering violation. In essence, the CPU is speculating that str x2 and ldr x4 are unrelated because doing so is very important for performance. Unfortunately, however, memory ordering violations are actually somewhat common and constantly having to restart execution has negative performance implication.

Now, this is actually a very similar problem as we'd see with conditional instruction speculation! So how do we solve this issue for memory ordering violations? Well, we predict which pairs of stores and loads are dependent and block the load from launching until the address of its supposed dependent store resolves. If this predictor is functioning well, we are able to both aggressively launch loads while also avoiding many costly fixups!

So, how would we translate this to conditional instruction speculation? Well, one idea is that we could predict both whether a given instruction is predictable and, if so, which way we should predict it. If a conditional instruction is predicted as unpredictable, its result will not be speculated (thereby avoiding frequent costly restarts) but if it is predicted to be predictable, we can try to predict which one to take.

Would this work? Maybe. Will anyone actually do this? Likely not. As others have suggested, conditional instructions are almost exclusively used for hard to predict conditions specifically because CPUs don't speculate them. Thus, in most existing code the predictor would just say "yep can't predict it" and we'd just have ended up wasting a bunch of area and power on a predictor that never gets used.

If you're really dedicated to this cause though, feel free to write a paper on it. Spitballing performance numbers is easy but often wrong in quite surprising ways, so maybe this might just work for some weird reason I've missed :)

Re:Six times faster than C by bjourne on Thursday July 06, 2023 @08:57PM (#36625582)
> Whereas conditional moves cannot execute any subsequent instructions until the correct value is available.

That is incorrect. Super-scalar processors have no problem executing subsequent instructions before the cmov writebacks. However, the register cmov writes to can of course not be read before cmov has has passed the execution unit. But that's not different from other arithmetic instructions.

Re:Six times faster than C by haberman on Friday July 07, 2023 @01:21AM (#36627524)
You are correct, I should have clarified, subsequent instructions that depend on the result of the cmov cannot execute until the cmov has executed. Whereas subsequent instructions that depend on the result of the branch instruction can be speculatively executed even before the branch conditional has been evaluated.
Re:Six times faster than C by bjourne on Friday July 07, 2023 @05:35AM (#36629299)
True, but independently of whether "cmov rax, ..." or "jnz L; mov rax, ...; L:" is used, subsequent instructions that reads rax needs to stall until rax has been written to (or at least until cmov/mov has executed if bypasses are used).
Re:Six times faster than C by NohatCoder on Friday July 07, 2023 @08:49AM (#36630912)
The difference is that in the case where the condition is false and predicted false the jump variant will not delay if the value being moved into rax is delayed, the cmov variant will. Effectively that value becomes a false dependency.

As best I can tell this case is rare enough that one shouldn't generally be afraid of cmov, and probably compiler authors should consider using it more frequently.

What one shouldn't do is to load values, that are likely in memory or L3, unnecessarily in order to be able to use cmov. It is the case that runs the greatest risk of degrading performance, and it puts extra load on resources that are shared between cores.

Re:Six times faster than C by bjourne on Friday July 07, 2023 @10:52AM (#36632625)
Oh, right! I totally forgot about that. I guess it (at least theoretically) could make a big difference in code for the abs function if the noop is the common case and also easily predictable.
Re:Six times faster than C by haberman on Friday July 07, 2023 @11:44AM (#36633461)
There is also the issue of the branch predicate itself. It is always a true dependency, but when is its value actually needed? For cmov, it is needed before dependent instructions can even be executed. For branch instructions, it is only needed before they can be retired. Speculative execution can keep the pipeline full in the meantime.
Re:Six times faster than C by AnotherGoodName on Thursday July 06, 2023 @07:10PM (#36624583)
The inputs here are random which is the problem and why this isn't demonstrating that. Create an input of all 's' and compare it.
Re:Six times faster than C by torstenvl on Thursday July 06, 2023 @07:15PM (#36624634)
Better than random input, but still only ~half as fast as using sete

    [19:13:34 user@boxer ~/src/looptest] $ diff -u bench.c bench-alls.c      
    --- bench.c 2023-07-06 16:04:16.000000000 -0400
    +++ bench-alls.c 2023-07-06 19:13:34.000000000 -0400
    @@ -17,7 +17,7 @@
       int num_rand_calls = number / CHAR_BIT + 1;
       unsigned char *buffer = malloc(num_rand_calls * CHAR_BIT);
       for (int i = 0; i < num_rand_calls; i++) {
    -    buffer[i] = rand();
    +    buffer[i] = 's'; //rand();
       }
       return buffer;
     }
    [19:13:37 user@boxer ~/src/looptest] $ gcc -O3 bench-alls.c loop2.s -o l2
    [19:13:42 user@boxer ~/src/looptest] $ gcc -O3 bench-alls.c loop4.s -o l4
    [19:13:47 user@boxer ~/src/looptest] $ time ./l2 1000 1
    250001000
    ./l2 1000 1  0.69s user 0.00s system 99% cpu 0.699 total
    [19:13:55 user@boxer ~/src/looptest] $ time ./l4 1000 1
    250001000
    ./l4 1000 1  1.28s user 0.00s system 99% cpu 1.290 total
Jumps are slower.
Re:Six times faster than C by Guvante on Thursday July 06, 2023 @10:26PM (#36626272)
Microbenchmarks are hard. You aren't doing any meaningful work that could benefit from speculatively executing instead of stalling for the conditional value.

Similarly you might be busting the pipeline by chaining together the jumps so close together.

Not saying your point is wrong, just saying your proof isn't super solid.

Re:Six times faster than C by torstenvl on Thursday July 06, 2023 @10:35PM (#36626348)
I'm sorry, but the claim is "You can always replace it by [a conditional jump over the mov instruction] and assuming the branch is AT ALL predictable the branch-over will actually be a LOT better for a CPU."

A single counterexample is rock solid proof against an "always" statement.

Re:Six times faster than C by gpderetta on Friday July 07, 2023 @05:11AM (#36629165)
In this benchmark the only loop carried dependency is over the res variable (edit: and of course the index). The jump doesn't break these dependencies, so for this specific problem, the additional latency of the cmov doesn't matter as it is always perfectly pipelined and cmov will always come up on top. But if the input of cmov depended on a previous value, then potentially a branch could be better given an high enough prediciton rate.
Re:Six times faster than C by seventhson on Thursday July 06, 2023 @08:52PM (#36625542)
Linus' post is 15+ years old. Much has changed in Intel hardware since then. He was probably right on the money re the hardware available at the time.
Re:Six times faster than C by torstenvl on Thursday July 06, 2023 @10:59PM (#36626528)
> I don't know when the change was made, but conditional moves are fast and efficient on the last several generations of AMD and Intel processors. Usually, you are trading 1 or 2 extra cycles of latency against the chance of a ~15 cycle mispredicted branch penalty. If your branch cannot be predicted correctly ~85% of the time, this can be a significant win.

https://news.ycombinator.com/item?id=10749195

Re:Six times faster than C by zaxomi on Friday July 07, 2023 @03:20AM (#36628336)
I read the rant. He is talking about Pentium 4.
Re:Six times faster than C by MobiusHorizons on Thursday July 06, 2023 @09:53PM (#36626003)
Jumps are slower on completely random input. If I understand Linus’s point correctly, he is suggesting that random inputs like this are unusual (although a good way to measure worst case performance)
Re:Six times faster than C by zaxomi on Friday July 07, 2023 @03:18AM (#36628317)
Did you test this on a Pentium 4, the processor that Linus is talking about?
Re:Six times faster than C by EVa5I7bHFq9mnYK on Friday July 07, 2023 @12:48AM (#36627280)
Is this the reason I dont usually see any speed up if I eliminate array boundary checking in C#? The jump condition is almost always false, is this what "predictable" means?
Re:Six times faster than C by gpderetta on Friday July 07, 2023 @02:40AM (#36628044)
Indeed.

The cost of bound checking is second order effects like making vectorization harder, slightly higher instruction (and possibly data) cache pressure, or requiring higher decode bandwidth. For the vast majority of programs these bottlenecks do not really matter.

Re:Six times faster than C by EVa5I7bHFq9mnYK on Friday July 07, 2023 @05:10AM (#36629156)
I mean, if the innermost loop is something like 3 assembly instructions, two extra instructions cmp and jg do not make any difference, if jg never executes?
Re:Six times faster than C by kaba0 on Friday July 07, 2023 @06:03AM (#36629509)
I am by no means an expert, but I believe what you have in mind would likely fit in i-cache without a problem, so you wouldn’t see a significant difference.

There is an interesting talk titled ‘the death of optimizing compilers’ that argues that for most code these optimizations are almost completely meaningless, and in the hot loops where it actually matters, they are not good compared to humans (and sometimes 100x or more improvements are possible and left on the table). While I don’t completely agree with its points, it is a good talk/slides to read through.

Re:Six times faster than C by EVa5I7bHFq9mnYK on Friday July 07, 2023 @04:53PM (#36638159)
Its not that compilers are stupid, they just dont know what humans know about their data, it's ranges, invariants, symmetries etc. They work on most general case, which can be horribly inefficient.
Re:Six times faster than C by gpderetta on Friday July 07, 2023 @06:10AM (#36629578)
If they are not in the critical path, it doesn't matter. There is no instruction cache issues as the loop is tiny. Also as the loop is tiny it will fit in the u-op cache (or even in the loop cache), so decoding is not an issue either. The only problem is potential lack of vectorization, but a good vector ISA can in principle handle the bound checking with masked reads and writes (but now the check is no longer a predictable branch, but it might end up in the critical path, although it is not necessarily a big cost, or even measurable, anyway).
Re:Six times faster than C by EVa5I7bHFq9mnYK on Friday July 07, 2023 @06:28AM (#36629679)
I thought I began to understand something, your rant proven me wrong) Thanks, anyway)
Re:Six times faster than C by gpderetta on Friday July 07, 2023 @07:16AM (#36630038)
Forget about the second order effects. The reason the extra instructions in first approximation do not matter is that loops typically are limited by carried loop dependencies.

Think about this: a machine with infinite execution units and memory bandwidth, potentially could execute all iterations of a loop at the same time, in parallel.

Unless each loop iteration depends somehow on the result of the previous iteration. Then only independent instructions of that iteration can execute in parallel and the loop is latency-chain bound (especially when it involves memory accesses). This is often the case. Because branch prediction breaks dependencies, bound checking is never part of a dependency chain, so it is often free or nearly so. For more optimized code, the assumption of infinite resources is of course not warranted and execution bandwidth and possibly even memory bandwidth need to be taken into consideration.

Re:Six times faster than C by 414owen on Thursday July 06, 2023 @06:16PM (#36623958)
Nice! There's a part two in which I rewrote the C. I got a 12x speedup :)

https://owen.cafe/posts/the-same-speed-as-c/

And as others have pointed out, you can tweak the input, then vectorize the algo, if you want to go that route.

I considered this a pedagogical exercise and I sincerely hope nobody will start dropping down to assembly without a very good reason to.

Re:Six times faster than C by JohnMakin on Thursday July 06, 2023 @08:14PM (#36625198)
Thank you for your post and reply but I fear with a post + title like this you may just be chumming the waters.
Re:Six times faster than C by jdsalaro on Thursday July 06, 2023 @08:29PM (#36625328)
What do you mean by "chumming the waters" in this context?
Re:Six times faster than C by bombcar on Thursday July 06, 2023 @08:35PM (#36625393)
People who just skim the headline and article will come away convinced that dropping to assembly is the “way to go fast” even if they never actually do it.
Re:Six times faster than C by sh34r on Thursday July 06, 2023 @09:55PM (#36626015)
Anyone with a passing understanding of Assembly or compilers would find that idea laughable. As for the others, it turns out not knowing what you don’t know can be very expensive.
Re:Six times faster than C by mayli on Thursday July 06, 2023 @08:32PM (#36625365)
"Clickbait"
Re:Six times faster than C by sriku on Thursday July 06, 2023 @08:51PM (#36625534)
Wondering how res += (c=='s')-(c=='p') might do. I sure there is some C undefined behaviour relevant there. Curious but too lazy to check it myself!
Re:Six times faster than C by Tempest1981 on Thursday July 06, 2023 @10:01PM (#36626053)
While `false` evaluates to 0, not sure `true` always evaluate to 1 in C... maybe compiler dependent. Maybe add `? 1 : 0`
Re:Six times faster than C by ladberg on Thursday July 06, 2023 @11:09PM (#36626585)
C doesn't even originally have true/false, I think you may be conflating the two concepts that "any nonzero int is truthy" and "boolean expressions evaluate to ints". The standard mandates that boolean expressions like equality always evaluate to 0/1.
Re:Six times faster than C by loeg on Thursday July 06, 2023 @11:18PM (#36626653)
The `true` constant is always 1. C11 §7.18 (3):

> true which expands to the integer constant 1,

And equality yields a 1 or 0. C11 §6.5.9 (3):

> The == (equal to) and != (not equal to) operators are analogous to the relational operators except for their lower precedence. Each of the operators yields 1 if the specified relation is true and 0 if it is false.

Re:Six times faster than C by romnon on Friday July 07, 2023 @01:08AM (#36627419)
ive seen people doing += !!(c=='s')-!!(c=='p') for that
Re:Six times faster than C by simonkagedal on Friday July 07, 2023 @03:31AM (#36628420)
That is entirely unneccessary. An == expression will always evaluate to 1 or 0. The !!x trick can be useful in some other situations, though.

Here’s a thing you could do (but I don’t know why):

+= !(c-’s’) - !(c-’p’)

Re:Six times faster than C by sweetjuly on Friday July 07, 2023 @03:38AM (#36628481)
I'm sure people do that (even though it's not necessary per some year C standard) but generally the pattern is actually for converting things which are not already 0 or 1 into 0 or 1. For example, you might want to use it here:

    int num_empty_strings = !!(strlen(s1)) + !!(strlen(s2)) + !!(strlen(s3))
which is equivalent to:

    int num_empty_strings = (strlen(s1) != 0) + (strlen(s2) != 0) + (strlen(s3) != 0)
Which you use is really a matter of coding style.
Re:Six times faster than C by stkdump on Friday July 07, 2023 @06:05AM (#36629525)
If we are being cryptic already, why not

    int num_empty_strings = !!*s1 + !!*s2 + !!*s3;
Re:Six times faster than C by gjm11 on Friday July 07, 2023 @07:59AM (#36630373)
That isn't only more cryptic, it's also potentially a lot more efficient -- strlen takes time proportional to the length of the string, which of course you don't need to do if you only care whether or not the length is zero. You shouldn't use strlen for empty-string tests.
Re:Six times faster than C by LegionMammal978 on Friday July 07, 2023 @08:32AM (#36630720)
In practice, GCC and Clang don't seem to have any issues inlining the necessary part of strlen at -O1 or higher (https://godbolt.org/z/rM198aYea). But MSVC inlines the empty-string case, while still calling out for nonempty strings, probably since it doesn't realize that the returned length will be nonzero.
Re:Six times faster than C by stkdump on Saturday July 08, 2023 @02:17AM (#36641830)
I guess since strlen uses an unsigned size, which has specified overflow behavior the compiler not only has to proof the initial iteration, but also all the ULLONG_MAX+1 multiples, which of course refer to the same memory address. But maybe its harder for the optimizer to see.
Re:Six times faster than C by areyousure on Friday July 07, 2023 @07:01AM (#36629912)
Note that your code computes the number of nonempty strings.
Re:Six times faster than C by rajnathani on Saturday July 08, 2023 @04:40AM (#36642560)
Great post!

One thought: If the code is rewritten using bit arithmetic, then potentially the result could be even faster as there need not be a pointer look-up.

A bit arithmetic solution would have a mask generated for the characters ‘p’ and ‘s’, and then the result could be AND-ed, and then with more bit arithmetic this all 1s value can be translated to a 1 if and only if all the bits are 1. Following which, there would be a no conditional check and simply be both an add and subtract operations but where the value to be added will only be 1 if the mask for ‘p’ matches and 1 if the mask for ‘s’ matches. I’m not fully sure if this would necessary be faster than the pointer look-up solution, but it would be interested to try this version of the code and see how fast it performs.

Re:Six times faster than C by p1necone on Thursday July 06, 2023 @06:47PM (#36624329)
Is rewriting switch statements to a bunch of ifs always faster? Or is there some number of cases where the switch is faster? Seems like it should be added as a compiler optimization if it's consistent.
Re:Six times faster than C by Karellen on Thursday July 06, 2023 @07:45PM (#36624941)
It shouldn't be.

If the fastest way to implement a particular `switch` in assembly is with the equivalent of a set of `if`s, a reasonably smart compiler "should" be able to output the assembly to do that. And I thought that gcc and clang at least have actually been smart enough to do that for a while now.

But if the number of `if`s is high and the distribution sufficiently dense, where a jump table is better than a bunch of `if`s, then a `switch` should output that.

OTOH, a sufficiently smart compiler could theoretically turn a bunch of `if`s into a `switch`-like jump table - but it's much harder to reason that case through correctly than it is the other way, so I'm not sure any current compilers are sufficiently smart to do that.

Re:Six times faster than C by kevin_thibedeau on Thursday July 06, 2023 @08:26PM (#36625306)
On x64, GCC is supposed to use a a jump table with more than 4 cases when the cases are dense enough (gaps of 9 or less) to minimize wasted memory, otherwise it generates sequential comparisons. Testing on Godbolt, it looks like GCC 13.1 uses 10 cases as the jump table threshold for ARM64.
Re:Six times faster than C by flohofwoe on Friday July 07, 2023 @02:57AM (#36628184)
It shouldn't if the control flow is actually identical.

E.g. note how both the switch- and if-based functions generate the same code using a lookup table here:

https://godbolt.org/z/qoT3M7r5G

Re:Six times faster than C by BbzzbB on Thursday July 06, 2023 @08:04PM (#36625110)
Is there any good article comparing performance across programming languages? Seems like everytime I see one they're broken because the tested logic is poorly implemented in language(s) XYZ.
Re:Six times faster than C by jtriangle on Thursday July 06, 2023 @08:09PM (#36625163)
It's a cardinal rule that any time someone utters "XYZ is n faster than C" someone comes along and shows C is actually 2x faster than XYZ.
Re:Six times faster than C by JohnMakin on Thursday July 06, 2023 @08:11PM (#36625176)
I had an old compilers professor say something like this once. “If you think you can do something better than the C compiler, I promise you you can’t.”
Re:Six times faster than C by kstrauser on Thursday July 06, 2023 @08:46PM (#36625493)
To a point. A modern C compiler generates mind boggingly fast assembler. However, some languages make it way easier to write sophisticated algorithms more easily.

For instance, suppose you're writing a program to find the nth Fibonacci number for whatever reason. In Python, the naive version might look like:

  def fib(n):
      if n <= 1:
          return n
      return fib(n - 1) + fib(n - 2)
On my machine, that takes about 12 seconds to find the 40th number. Altering that slightly like:

  from functools import cache
  @cache
  def fib(n): ...
makes the whole program take about 30 milliseconds total. The 400th takes about 32ms and emits an answer that won't fit in a 256-bit int.

Of course you can do the exact same kind of caching in C! I mean, the main Python interpreter's written in C, so by extension any algorithm you can express in Python you can also express in C. It'd probably be a lot faster, too!

But in practice, if I'm writing that in Python, I can use the obvious algorithm, spent 10 seconds slapping a caching decorator on it, verify that the end result is ridiculous fast and efficient, then move on to other problems.

Any reasonable C compiler will emit assembler that's vastly better than anything I could come up with. Conversely, I personally can write far better algorithms in Python than I could in C, because it's easier for me to express cleverness in that language. Those algorithmic improvements tend to have a far better speed payoff than I'd personally get from a more efficient implementation of a crappy method.

Re:Six times faster than C by hinkley on Thursday July 06, 2023 @09:31PM (#36625840)
How many brains has the Fibonacci example broken...

You'd unroll it to a loop on both C and Python. Fibonacci doesn't need a cache. It needs K previous values, where K=1.

Re:Six times faster than C by kstrauser on Thursday July 06, 2023 @10:14PM (#36626174)
Yeah, I almost said “suppose you were writing a Fibonacci program because who did you annoy to make this your life now...”

Like, obviously you’re not going to be writing `fib(n)` for real. I still claim that other languages — not just Python, either — make it easier to express cleverer algorithms than C does. You can’t write anything in Rust you can’t write in C, but it’ll probably be easier to say it more efficiently, and more correctly, in Rust. And much of the time, using a better design is going to blow compiler improvements out of the water.

(The professor was right if you limit the scope of the statement to “programs written in C or assembler”, of course. Unless you’re a freaking genius, a compiler’s going to write better object code.)

Re:Six times faster than C by adament on Friday July 07, 2023 @04:50AM (#36629036)
If for some reason you really wanted to compute Fib(n) for ridiculously large numbers of n, you would probably use that [Fib(n), Fib(n-1)] = A [Fib(n-1), Fib(n-2)] for the transition matrix A = [[1, 1], [1, 0]] and thus [Fib(n+1), Fib(n)] = A^n [Fib(1), Fib(0)] and then use exponentiation by squaring to compute A^n directly and thus Fib(n) in log_2(n) steps.
Re:Six times faster than C by olliej on Friday July 07, 2023 @05:13AM (#36629179)
adding @cache meaningfully changes the algorithmic complexity from O(1.8^^N) (iirc - it's obviously exponential) to O(N).
Re:Six times faster than C by kstrauser on Friday July 07, 2023 @11:04AM (#36632824)
Yep. That’s what I mean about cleverer algorithms. And while you could certainly do the exact same thing in C, it wouldn’t be a one-line change you could casually add and move on from.
Re:Six times faster than C by saagarjha on Friday July 07, 2023 @12:36AM (#36627195)
Someone has to teach the compiler how to be clever.
Re:Six times faster than C by zokier on Friday July 07, 2023 @02:24AM (#36627938)
That is just dumb and overly reductive. There is just lot of properties about programs that can not be expressed to compiler with portable ISO C but could be exploited for optimization. Of course reducing portability progressively improves situation, like using GCC extensions or machine-specific intrinsics. But even then its ridiculous to claim that C compilers could in general case generate true optimal code.
Re:Six times faster than C by okaleniuk on Friday July 07, 2023 @04:10AM (#36628710)
Yes, but this will backfire on ARM, where jumps are as roughly fast as conditional arithmetic.

The whole point of using C is not to think about the underlying architecture. As soon as you start taking "jumps are a lot slower than conditional arithmetic on x86" into account, you're not writing in C, you're writing in assembly with extra steps :-)

Re:Six times faster than C by bruce343434 on Friday July 07, 2023 @07:57AM (#36630354)
Is that because cjumps on ARM are faster, or cmovs on ARM are slower?
Re:Six times faster than C by dzaima on Friday July 07, 2023 @09:44AM (#36631648)
Note, that's only ARMv7; ARMv8 dropped most of the conditionally executed instruction stuff. And so it isn't even jumps that's fast on ARMv7, it's specifically cases that can be (and are) converted to predicated instrs; jumps are still gonna be slow in general on anything high-perf enough that it needs speculation, which can include the actual jumps of ARMv7.

If a compiler can convert jumpy code to the predicated instrs, it should be able to trivially convert conditional arith to such too (even easier & more consistently than branches I'd say).

Re:Six times faster than C by MisterTea on Friday July 07, 2023 @09:43AM (#36631636)
You can shorten the loop to just tree conditionals:

  while (c = *input++) {
      if (c == 's') r++;
      if (c == 'p') r--;
  }