21

I'm looking for some table or something similar that could help me to calculate efficiency of assembly code.

As I know bit shifting takes 1 CPU clock, but I really looking how much takes addition (subtraction should take the same), multiplication and how to presumably calculate division time if I know values that are dividing.

I really need info about integer values, but float execution times are welcome too.

1

4 Answers 4

19

In general, each of these operations takes a single clock cycle as well to execute if the arguments are in registers at the various stages of the pipeline.

What do you mean by latency? How many cycles an operation spends in the ALU?

You might find this table useful: http://www.agner.org/optimize/instruction_tables.pdf

Since modern processors are super scalar and can execute out of order, you can often get total instructions per cycle that exceed 1. The arguments for the macro command are the most important, but the operation also matters since divides take longer than XOR (<1 cycle latency).

Many x86 instructions can take multiple cycles to complete some stages if they are complex (REP commands or worse MWAIT for example).

2
  • 7
    Integer multiply is at least 3c latency on all recent x86 CPUs (and higher on some older CPUs). On many CPUs it's fully pipelined, so throughput is 1 per clock, but you can only achieve that if you have three independent multiplies in flight. (FP multiply on Haswell is 5c latency, 0.5c throughput, so you need 10 in flight to saturate throughput). Division (div and idiv) is even worse: it's microcoded, and much higher latency than add or shr, and not even fully pipelined on any CPU. All of this is straight from Agner Fog's instruction tables, so it's a good thing you linked that. Commented Jul 6, 2017 at 17:41
  • See also Why is this C++ code faster than my hand-written assembly for testing the Collatz conjecture? for more about optimizing asm. Commented Jul 6, 2017 at 17:44
8

Calculating the efficiency of assembly code is not the best way to go in these days of Out of Order Execution Super Scalar pipelines. It'll vary by processor type. It'll vary on instructions both before and after (you can add extra code and have it run faster sometimes!). Some operations (division notably) can have a range of execution times even on older more predictable chips. Actually timing of lots of iterations is the only way to go.

5
  • I know that, but I need that not in real project but in one kind a fun programming project. Commented Sep 10, 2013 at 19:00
  • Whether you need it for real or for fun doesn't change the answer for this processor line. Have you considered switching to a more deterministic processor, such as a Propeller chip, instead? Commented Sep 10, 2013 at 20:22
  • 3
    Even with a scalar, in-order implementation branch mispredictions and cache misses can cause variation in run time. Commented Sep 11, 2013 at 11:21
  • 1
    For purely CPU-bound stuff (no cache misses, no branch mispredicts), CPU behaviour is understood in enough detail that static analysis can often predict almost exactly how many cycles per iteration a loop will take on a specific CPU (e.g. Intel Haswell). e.g. see this SO answer where looking at the compiler-generated asm let me explain why the branchy version ran almost exactly 1.5x faster than the CMOV version on the OP's Sandybridge CPU, but much closer on my Skylake. Commented Jul 6, 2017 at 18:05
  • If you're writing asm by hand for performance reasons, then it is actually useful to look for latency and throughput bottlenecks on Intel and AMD CPUs. It's hard, though, and sometimes what's optimal for AMD isn't what's optimal for Intel. Commented Jul 6, 2017 at 18:08
4

You can find information on intel cpu at intel software developer manuals. For instance the latency is 1 cycle for an integer addition and 3 cycles for an integer multiplication.

I don't know about multiplication, but I expect addition to always take one cycle.

4
  • One cycle, except when it's "free" (in parallel when pipelines line up correctly) or takes longer due to a cache miss. :-) Commented Sep 24, 2013 at 18:18
  • 4
    Currently (2018) this information is available in Appendix C named "Instruction Latency and Throughput" of document 248966 "Intel® 64 and IA-32 Architectures Optimization Reference Manual" also available on the page linked in the answer Commented Feb 7, 2018 at 13:04
  • Arrow Lake has 3 multiplies per cycle. What latency I dunno though. But inverse throughput is 3. Commented Jan 2 at 5:52
  • Latency is also 3. Commented Mar 1 at 4:48
0

uops.info

https://uops.info seems to be one of the best resources that exist for this. It contains the results of open source microbenchmarks on various Intel and AMD CPUs, notably establishing instruction latency and throughput.

E.g. this is the results page for IMUL rax, rax: https://uops.info/html-instr/IMUL_R64_R64.html E.g. if you were interested in AMD Zen 4 CPUs https://uops.info/html-instr/IMUL_R64_R64.html#ZEN4 tells us that:

  • latency: 3. I.e.: running one IMUL instruction takes 3 cycles to finish.

    Also note that some instructions have different latencies for different outputs, e.g. MUL produces its RAX output in 3 cycles, but takes 4 cycles to produce RDX output: https://uops.info/html-instr/MUL_R64.html#ZEN4

  • throughput: 1: if you run a bunch of independent IMUL instructions, it is able to pipeline things and run one IMUL per cycle on average.

We could also look e.g. at INC https://uops.info/html-instr/INC_R64.html#ZEN4 for comparison:

  • latency: 1
  • throughput: 0.25, i.e. the CPU can run 4 independent INC per cycle

The minimal experiments below may also help to make these concepts clearer.

Simple experimental setup that may give a meaningful result: determining the latency of MUL

As others have emphasized, it can be hard to determine what is going on due to various microarhitectural features, and limitations of how precisely your experimental assembly can poke at them without triggering other effects. But here's a simplistic experimental attempt at determining the latency of the MUL multiplication:

main.c

#include <stdlib.h>
#include <stdint.h>

int main(int argc, char **argv) {
    uint64_t max, i, x0;
    if (argc > 1) {
        max = strtoll(argv[1], NULL, 0);
    } else {
        max = 1;
    }
    i = max;
    x0 = 1;
#if defined(__x86_64__) || defined(__i386__)
    __asm__ (
        "mov %[x0], %%rax;"
        "mov $2, %%rbx;"
        "loop:"
        "mul %%rbx;"
        "dec %[i];"
        "jne loop;"
        "mov %%rax, %[x0];"
        : [i] "+r" (i),
          [x0] "+r" (x0)
        :
        : "rax",
          "rbx",
          "rdx"
    );
#endif
    return x0;
}

Also, a quick reminder of how MUL works, doing e.g.:

mul %rbx

multiplies:

RBX * RAX

and stores the result in two fixed registers:

  • RDX: top 64 bits (as the results of multiplying two 64-bit numbers can have up to 128 bits)
  • RAX: lower 64 bits

Then compile and time it:

gcc -ggdb3 -O0 -std=c99 -Wall -Wextra -pedantic -o main.out main.c
time ./main.out 5000000000

I choose a value of 5 billion loops because I know that this is more or less the frequency of my AMD Ryzen 7 7840U CPU ([Zen 4 microarchitecture](Zen 4)), so it will give results of the order of 1 second, which we hope will average well enough without taking too much time. You can find your CPU frequency on Linux as mentioned at: https://askubuntu.com/questions/218567/any-way-to-check-the-clock-speed-of-my-processor

The result was almost exactly 3 seconds:

real    0m3.014s
user    0m3.010s
sys     0m0.003s

Now, let's remove the "mul %%rbx;" multiplication from the loop to see how long it takes without it:

        "loop:"
        "inc %[i];"
        "cmp %[max], %[i];"
        "jb loop;"

I get about 1s, as expected running a tiny trivial loop at 1 iteration per cycle:

real    0m1.006s
user    0m1.005s
sys     0m0.001s

This therefore suggests that MUL took 3 cycles, as many sources suggest. It's likely not 2 cycles (the difference between INC and MUL) because we know that the CPU likely has separate functional units to run INC and MUL, so that when superscalar execution is running, both instructions can be executed at the same time:

+---------+
| CPU     |
|         |
| +-----+ |
| | ADD | |
| +-----+ |
|         |
| +-----+ |
| | MUL | |
| +-----+ |
|         |
+---------+

and then MUL just ends up taking the longest and dominating the overall runtime.

Estimating how many simultaneous MUL the CPU can do

Next, for fun, we can try to estimate how many MUL functional units the CPU has, which determines how many MUL it can do at once.

To do that, let's use assembly of the form:

__asm__ (
    "mov $2, %%rbx;"
    "loop:"

    "mov %[x0], %%rax;"
    "mul %%rbx;"
    "mov %%rax, %[x0];"

    "mov %[x1], %%rax;"
    "mul %%rbx;"
    "mov %%rax, %[x1];"

    "dec %[i];"
    "jne loop;"
    : [i] "+r" (i),
      [x0] "+r" (x0),
      [x1] "+r" (x1)
    :
    : "rax",
      "rbx" ,
      "rdx" 
);

Here we well test if the CPU can do 2 MUL instructions simultaneously.

We add some MOV instructions to ensure that the input from the second MOV, which uses RAX, does not depend on the output of the previous one, so that the CPU can get a chance to run them in parallel.

You may also object that they can't possibly run in parallel as both take input from and output to RAX. But that is not true because CPUs have register renaming to take care exactly of this type of issue. The CPU is able to notice that both RAX usages are independent, and use different actual memory locations for both, even though both in principle modify RAX.

When I benchmark this I get once again about 3 seconds:

real    0m3.091s
user    0m3.087s
sys     0m0.002s

so we conclude that the CPU managed to run two MUL "in parallel", without extra overhead. There are a few possible explanations for this:

  1. the CPU has two MUL ALUs, and MUL executes in either of them over three cycles
  2. the CPU has one MUL ALU, and MUL is split up into several uops, only one of which uses the MUL ALU. The 3 cycle latency exists because the multiple uops must execute in series. The MUL ALU uop runs in one cycle. Two instructions manage to run at once because they take turns using the MUL ALU while the other is doing the other uops
  3. the MUL ALU itself is pipelined. A MUL uop takes 3 cycles, but 3 can run at the same time in different stages.

We can try to decide between those two options by increasing the number of independent MUL instructions in the loop to see how many MUL the CPU can do per second (throughput). I did that using this helper script and the final plot was:

enter image description here

This plot excludes the possibility 1) in which there are two MUL ALUs each taking 3 cycles because from the inclination of the curve, for each MUL that we add to the loop, it only adds 1 second to the runtime. Therefore the CPU is able to run one MUL per cycle on average.

If we had 2 MUL ALUs taking 3 cycles each, then it would instead average to 1 MUL over 1.5 cycles.

It is not easy to decide between 2 and 3 from this graph alone.

The one thing that is not so neat about the experiment is that we weren't able to run 3 MUL in the same time as 2, given that the 3 cycle latency would suggest to us that this is possible, but seems not to happen due to interference in how the experiment is structured.

Doing this experiment with the simpler IMUL instruction which can output to a single register of our choice only without MOV e.g.:

IMUL $3, %rax, %rax

we obtain instead:

enter image description here

and the expected 3 instruction plateau is perfectly visible. I've also added a INC instruction for fun.

The following resources contain other reverse engineering efforts:

Tested on Ubuntu 25.04.

Related:

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.