Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Unexpected run time difference

Tags:

c

Consider the given two cases,

In this case below I am just running two nested loops both initialized from 0 and running upto 100000.

int k = 100000;
for(i=0;i<k;i++)
    for(j=0;j<k;j++){
    // Do nothing
 }

time on my system = 22.6 seconds

Again I am doing the same thing, just incrementing a variable c inside.

int k = 100000, cnt=0;
for(i=0;i<k;i++)
    for(j=0;j<k;j++){
    cnt++;
 }

time on my system = 19.6 seconds

How come ??? Why is the time in case2 < case1 ??

like image 692
aiccha Avatar asked Jul 23 '26 14:07

aiccha


1 Answers

I just reproduced the results, and asked myself the same question as the OP.

here is the code:

>>>> test1.c
int
main ()
{
  long long int i;
  long long int j;
  long long int k = 100000;
  for(i=0;i<k;i++)
    for(j=0;j<k;j++)
      {
        // Do nothing
      }

  return 0;
}

.

>>>> test2.c
int
main ()
{
  long long int i;
  long long int j;
  long long int c = 0;

  long long int k = 100000;
  for(i=0;i<k;i++)
    for(j=0;j<k;j++)
      {
        c++;
      }

  return 0;
}

compiled with gcc -o testx testx.c -g on an amd64 gentoo linux machine. When run, I get the following times:

  test1: 0m32.000s
  test2: 0m28.307s

I have tested this multiple times, and the derivation is surprisingly small.

To understand what happens here, we have to look at the disassembly.

>>>> test1
Dump of assembler code for function main:
   0x00000000004004fc <+0>:     push   %rbp
   0x00000000004004fd <+1>:     mov    %rsp,%rbp
   0x0000000000400500 <+4>:     movq   $0x186a0,-0x18(%rbp)
   0x0000000000400508 <+12>:    movq   $0x0,-0x8(%rbp)
   0x0000000000400510 <+20>:    jmp    0x400530 <main+52>
   0x0000000000400512 <+22>:    movq   $0x0,-0x10(%rbp)
   0x000000000040051a <+30>:    jmp    0x400521 <main+37>
   0x000000000040051c <+32>:    addq   $0x1,-0x10(%rbp)
   0x0000000000400521 <+37>:    mov    -0x10(%rbp),%rax
   0x0000000000400525 <+41>:    cmp    -0x18(%rbp),%rax
   0x0000000000400529 <+45>:    jl     0x40051c <main+32>
   0x000000000040052b <+47>:    addq   $0x1,-0x8(%rbp)
   0x0000000000400530 <+52>:    mov    -0x8(%rbp),%rax
   0x0000000000400534 <+56>:    cmp    -0x18(%rbp),%rax
   0x0000000000400538 <+60>:    jl     0x400512 <main+22>
   0x000000000040053a <+62>:    mov    $0x0,%eax
   0x000000000040053f <+67>:    pop    %rbp
   0x0000000000400540 <+68>:    retq   
End of assembler dump.

.

>>>> test2:
Dump of assembler code for function main:
   0x00000000004004fc <+0>:     push   %rbp
   0x00000000004004fd <+1>:     ov    %rsp,%rbp
   0x0000000000400500 <+4>:     movq   $0x0,-0x18(%rbp)
   0x0000000000400508 <+12>:    movq   $0x186a0,-0x20(%rbp)
   0x0000000000400510 <+20>:    movq   $0x0,-0x8(%rbp)
   0x0000000000400518 <+28>:    jmp    0x40053d <main+65>
   0x000000000040051a <+30>:    movq   $0x0,-0x10(%rbp)
   0x0000000000400522 <+38>:    jmp    0x40052e <main+50>
   0x0000000000400524 <+40>:    addq   $0x1,-0x18(%rbp)
   0x0000000000400529 <+45>:    addq   $0x1,-0x10(%rbp)
   0x000000000040052e <+50>:    mov    -0x10(%rbp),%rax
   0x0000000000400532 <+54>:    cmp    -0x20(%rbp),%rax
   0x0000000000400536 <+58>:    jl     0x400524 <main+40>
   0x0000000000400538 <+60>:    addq   $0x1,-0x8(%rbp)
   0x000000000040053d <+65>:    mov    -0x8(%rbp),%rax
   0x0000000000400541 <+69>:    cmp    -0x20(%rbp),%rax
   0x0000000000400545 <+73>:    jl     0x40051a <main+30>
   0x0000000000400547 <+75>:    mov    $0x0,%eax
   0x000000000040054c <+80>:    pop    %rbp
   0x000000000040054d <+81>:    retq   
End of assembler dump.

Which, as expected, looks very similar.

I have highlighted what the code does in a commented version of test2 below. The indentation of the assembly lines represents the level of the loop they are in, or that they implement.

>>>> test2:
Dump of assembler code for function main:
   // setup the stackframe
   0x00000000004004fc <+0>:     push   %rbp
   0x00000000004004fd <+1>:     ov    %rsp,%rbp
   // initialize variable c
   0x0000000000400500 <+4>:     movq   $0x0,-0x18(%rbp)
   // initialize variable k
   0x0000000000400508 <+12>:    movq   $0x186a0,-0x20(%rbp)
     // initialize variable i
     0x0000000000400510 <+20>:  movq   $0x0,-0x8(%rbp)
     // enter the outer loop
     0x0000000000400518 <+28>:  jmp    0x40053d <main+65>
       // initialize variable j
       0x000000000040051a <+30>:    movq   $0x0,-0x10(%rbp)
       // enter the inner loop
       0x0000000000400522 <+38>:    jmp    0x40052e <main+50>
         // increment variable c
         0x0000000000400524 <+40>:  addq   $0x1,-0x18(%rbp)
       // increment variable j
       0x0000000000400529 <+45>:    addq   $0x1,-0x10(%rbp)
       // check if the inner loop condition still holds
       0x000000000040052e <+50>:    mov    -0x10(%rbp),%rax
       0x0000000000400532 <+54>:    cmp    -0x20(%rbp),%rax
       // jump to the start of the inner loop, if true, else continue
       0x0000000000400536 <+58>:    jl     0x400524 <main+40>
     // increment variable i
     0x0000000000400538 <+60>:  addq   $0x1,-0x8(%rbp)
     // check if the outer loop condition still holds
     0x000000000040053d <+65>:  mov    -0x8(%rbp),%rax
     0x0000000000400541 <+69>:  cmp    -0x20(%rbp),%rax
     // jump to the start of the outer loop, if true, else continue
     0x0000000000400545 <+73>:  jl     0x40051a <main+30>
   // tear down and return to main
   0x0000000000400547 <+75>:    mov    $0x0,%eax
   0x000000000040054c <+80>:    pop    %rbp
   0x000000000040054d <+81>:    retq   
End of assembler dump.

As you can see, the code structure is very similar to the actual C code, and the differences between the assembly of test1 and test2 are very little.

The reason why test2 performs marginally faster is probably buried deeply in the specification of your hardware. I think it could be possible that modern processors have optimized instruction caching and pipelining for simple loops, because these are so commonly found in programs, and that the optimization does not apply to empty loops, as they are (1) very rare in actual programs and (2) runtime optimization does not actually matter for empty loops, as they are usually intended for (busy) waiting.

Whatever the reason, as academically interesting it might be, The impact on real software would probably be nonexistent :)

I just found this document published by Intel, that should be an interesting read, if you are interested in the details http://www.google.de/url?sa=t&rct=j&q=&esrc=s&source=web&cd=4&cad=rja&ved=0CFgQFjAD&url=http%3A%2F%2Fwww.agner.org%2Foptimize%2Fmicroarchitecture.pdf&ei=8-sVUtWyM8nPtAb4ooCQBQ&usg=AFQjCNGRPm4A8ixWqSSGOOtNPCxp1YRfQg&sig2=Qe6Nxmz4Lee5Oo8UOGwTJw&bvm=bv.51156542,d.Yms

like image 188
Andreas Grapentin Avatar answered Jul 26 '26 07:07

Andreas Grapentin