Benchmark fără scop

Nu sunt foarte sigur unde s-ar încadra chestia asta, probabil la o categorie de genul “Ce efect au razele Lunii asupra papucilor de gumă” :slight_smile:

Din curiozitate plictisită, m-a pălit ideea să văd cu cât mai rapid poate fi executat o secvenţă simplă scrisă în assembler, faţă de o secvenţa care execută ceva echivalent, dar scrisă în C. Am făcut o buclă care execută 1,000,000,000 (un miliard) iteraţii şi incrementează o variabilă. A ieşit un cod de genul ăsta:

http://cpp.sh/2nsnu

#include <stdio.h>
#include <time.h>

static clock_t time_start;
static int result;

void start()
{
    time_start = clock();
}

void elapsed(const char *msg)
{
    clock_t time_end = clock();
    fprintf(stdout, "%s: %d ms\n", msg, (time_end - time_start) / (CLOCKS_PER_SEC / 1000));
    fprintf(stdout, "    RESULT: %d\n", result);
}


int main(int argc, char *argv[])
{
    result = 0;

    start();
    for(int i = 1000000000; i != 0; i--)
    {
        result++;
    }
    elapsed("Test C");

    start();
    __asm__ volatile
    (
        "xor %%eax, %%eax;"
        "mov $1000000000, %%ecx;"

        "LOOP:"
        "inc %%eax;"
        "dec %%ecx;"
        "jnz LOOP;"

        "mov %%eax, %0;"

        : "=r" (result)
        :
        : "%ecx"
    );
    elapsed("Test ASM");

    return 0;
}

Maşina de test este un SBC de la PC Engines, un bătrân ALIX:

processor       : 0
vendor_id       : AuthenticAMD
cpu family      : 5
model           : 10
model name      : Geode(TM) Integrated Processor by AMD PCS
stepping        : 2
cpu MHz         : 498.050
cache size      : 128 KB
fdiv_bug        : no
hlt_bug         : no
f00f_bug        : no
coma_bug        : no
fpu             : yes
fpu_exception   : yes
cpuid level     : 1
wp              : yes
flags           : fpu de pse tsc msr cx8 pge cmov clflush mmx mmxext 3dnowext 3dnow up
bogomips        : 996.10

Rezultatul… well, aici e mai complicat. Dacă setam compilatorul să nu optimizeze codul (flag -O0), codul ASM este executat de cel puţin două-trei ori mai rapid decât cel scris în C (asta este rulat pe o maşină ALIX, cu CPU

 Test C: 18400 ms
     RESULT: 1000000000
 Test ASM: 6140 ms
     RESULT: 1000000000

Diferenţa mare este cauzată de faptul că secvenţa în assembler foloseşte doar regiştrii interni ai procesorului, în schimb codul generat de C accesează şi RAM-ul.

În schimb, dacă optimizăm cu -O2, rezultatele se schimbă drastic:

Test C: 0 ms
    RESULT: 1000000000
Test ASM: 6110 ms
    RESULT: 1000000000

Explicaţia e simplă, optimizerul compilatorului vede că este o corelaţie între numărul de interaţii şi incrementarea variabilei “result”. Şi atunci pur şi simplu pune în “result” numărul de iteraţii, nici măcar nu se mai oboseşte să execute bucla. Am incercat să-l păcalesc, sa introduc numărul de interaţii şi step-ul pentru increment prin stdin, să nu le ştie de la compile-time. Degeaba, tot gaseşte o corelaţie şi execută direct asignarea :slight_smile: (probabil îşi dă seama că rezultatul final este “numărul de iteraţii * step”).

Totuşi, este impresionant că un CPU la doar 500 MHz este capabil să execute 1 miliard (!) de iteraţii în doar 6 secunde. Pe un CPU I7 rezultalte sunt astea:

Test C: 1696 ms
    RESULT: 1000000000
Test ASM: 239 ms
    RESULT: 1000000000
5 Likes

Ca să-mi fac o idee de diferenţa de ordin de mărime între ASM/C şi PHP (ver. 5.3.3), am creat un test asemănător:

#!/usr/bin/php
<?php

$result = 0;

$start = time();

for($i = 1000000000; $i !=0; --$i)
    $result++;

$minutes = (int)((time() - $start) / 60);
echo "Test PHP: $minutes min\n";
echo "    RESULT: $result\n";

Rezultatul e cam trist :slight_smile:

Test PHP: 22 min
    RESULT: 1000000000

Pe maşina cu CPU i7 e ceva mai bine, php-ul (ver. 7.3.11) se execută în doar 9 secunde. Da, dar aici ASM-ul se execută în 239 ms…

2 Likes

Dacă încă te plictisești, aș fi curios de Java, Node, Go

1 Like

Să vad dacă există pachete pentru Centos 5.

Până acum de departe cel mai prost e Python, pe i7 durează 48 de secunde (!). Probabil pe ALIX va dura câteva ore. Sau poate n-am scris eu corect testul, poate e cineva familiarizat cu Python şi îmi poate da o idee de optimizare.

#!/usr/bin/python

import time

result = 0;

start_time = time.time()
for i in xrange(1000000000):
    result += 1

elapsed_time = (time.time() - start_time);

print("Test Python: " + str(time.strftime("%M:%S", time.gmtime(elapsed_time))) + " seconds")
print("    RESULT: " + str(result))
Test Python: 00:48 seconds
    RESULT: 1000000000

Bag eu acasa in java si c# :wink:

1 Like

Trebuie să facem testele pe aceeaşi maşină, ca să avem un punct de reper. Eu am luat ALIX ca referinţă pentru că testele ASM/C pe un CPU modern se execută prea repede şi rezultatele nu era foarte concludente.

De fapt mi-a venit o idee. Hai să facem un soi de contest. Fiecare să facă în limbajul pe care îl stăpâneşte cel mai bine un program care să citească dintr-un fişier câteva zeci de milioane de perechi de numere aleatoare, pe care să le împartă între ele şi să scrie rezultatele împărţirii într-un alt fişier. În felul ăsta avem de toate, şi I/O şi calcule complexe.

Fă un repo pe github…

Fac acum un generator de numere aleatoare si fac un proof-of-concept in C pentru ce trebuie sa faca programul de test.

@Silviu, @serghei

[C#]

using System;
using System.Diagnostics;

namespace Bench
{
    class Program
    {
        static void Main(string[] args)
        {
            int result = 0;

            var sw = new Stopwatch();
            sw.Start();

            for (int i = 1000000000; i != 0; i--)
            {
                result++;
            }
            Console.WriteLine("Value: {0} Took: {1}s", result, sw.ElapsedMilliseconds / 1000F);
        }
    }
}

[Java]

package com.company;

public class Main {

    public static void main(String[] args) {
	// write your code here

        int result = 0;

        var start = System.currentTimeMillis();

        for (int i = 1000000000; i != 0; i--)
        {
            result++;
        }
        var took = (System.currentTimeMillis() - start) / 1000f;

        System.out.println(result);
        System.out.println(took);
    }
}

Timpi de executie din ide:

  • Visual Studio ~1.4-1.5 secunde cu Debugger-ul atasat si ~0.5 secunde cu profilul de Release
    *IntelliJ ~0.5 secunde

Cred ca in cazul Visual Studio exista si un overhead cauzat poate de ide. In cazul Visual Studio este selectata profilul Debug. Am schimbat in Release si timpul este comparabil cu cel din java

Vreau sa le rulez din cli si sa adaug si un benchmark


https://openjdk.java.net/projects/code-tools/jmh/

1 Like

Le execută cam repede, cel mai probabil e optimizată execuţia şi nu execută deloc loop-ul. Sunt curios cum se vor descurca C# şi Java în benchmark-ul de aici.

$ cat forloopsum.go 
package main

import (
        "fmt"
        "time"
)

func main() {
        var sum, i int64
        start := time.Now()
        for ; i < 1000000000; i++ {
                sum += i
        }
        stop := time.Now()
        fmt.Printf("it took %+v\n", stop.Sub(start))
}

$ go build forloopsum.go 
$ ./forloopsum
it took 305.848289ms

1 Like

La mine codul tău se execută in 285 ms, aproape la fel ca versiunea în assembler (239 ms). Interesant însă că go nu s-a prins că loop-ul ăla poate fi optimizat. C-ul pur şi simplu îl scurcircuitează, timpul de execuţie e zero.

clojure:

(time (reduce +(range 1000000000)))
"Elapsed time: 7388.640885 msecs"

De foarte multe ori optimizarile facute de catre compilator sunt mai bune decat ce face programatorul in asamblare.