1.) Wenn man einfach etwas sinnloses in einer for-Schleife laufen lässt, dann kann man ganz schön überrascht werden. Ein guter Compiler wird dir da einfach alles streichen, was er für nicht sinnvoll erachtet. Beispiel in Visual C++:
#define max 1024*1024*1024
for(uint32 i=0;i<max;i++)
{
for(uint32 j=0;j<max;j++);
}
Was glaubt ihr, wie lange das läuft? Genau 0ms, weil der Compiler das wegoptimiert. IN VB.NET ca. 7 Takte/Durchlauf
Weiteres Beispiel:
#define max 1024*1024*1024
byte *data=new data[];
for(uint32 i=0;i<max;i++)
data=123;
Wie lange läuft die Schleife? Immerhin tut er ja was. Wenn man es einfach 1:1 in Assembler zerlegt kommt ca. sowas raus (sehr vereinfacht, ich weiß dass es sich so nicht ausführen lässt):
'ANFANG
loadx data 'Cache Zugriff
loady i 'Cache Zugriff
addx y
store 123 dwordx 'Cache Zugriff
loadx i 'Cache Zugriff
comparex max
branchifx ANFANG
'ENDE
Sollten also mindestens 10-20 Takte sein (Cache Zugriff ist normal mindestens 3 Takte). In Wirklichkeit läuft es bei mir ca. 100ms (bei 10GB/s RAM-Bandbreite). In VB.NET wird das sicher auch deutlich langsamer gehen.
C# ist so eine Mischung zwischen C/C++ und VB.NET, also wird es damit auch um ein Eck langsamer gehen.
2.) Wenn du einen Benchmark machen willst, dann brauchst du eine halbwegs reale Anwendung, sonst testest du nur den Compiler. Mit einem 10-Zeiler wirst du damit nicht auskommen. Der richtige Unterschied kommt ja erst zum Tragen, wenn man alle Features des OS wirklich ausschöpft z.B. 4 Threads parallel, synchronisierte Speicherzugriffe, Threads starten/stoppen, neue Prozesse erzeugen, Daten von einem Prozess zum anderen Schaufeln mit Pipes, Implementierung von DirectX und OpenGL mit ihren jeweiligen Features, Datenbankzugriffe, Zugriffe auf Festplatten random, sequentiell, kopieren von einer Platte zur anderen etc, da das Caching auch des OS macht.
Bei einer CPU kommt es hauptsächlich darauf an, wie gut sie ihren Cache verwalten kann. Du kannst also nicht nur 1KB an Daten bearbeiten, alles sequentiell oder random Reads über den RAM (wo kein Data Prefetching stattfinden kann) usw. Ein gutes Data Prefetching erkennt z.B. Zugriffe, die in regelmäßigen Intervallen quer verteilt über den Speicher erfolgen z.B. alle 815Bytes auslesen. Dann kommt auch noch die Branch Prediction zum Tragen, also kurze Schleifen dürfen nicht sein, die sich oft wiederholen oder per Zufallszahl gesteuert. Selbst die Ergebnisse von SPEC kann man in die Tonne treten, obwohl das schon konkrete Anwendungen sind. Es gibt so viele Effekte, an die man vorher nie gedacht hat. Ich habe mir z.B. gedacht ich mache mir einen schnellen Zufallszahlenalgorithmus, wo ich die Bits irgendwie quer vertausche und füge das in einen Baum ein. Dann habe ich einen echten Algorithmus genommen und siehe da es war 3 mal so langsam, obwohl man vorher schon nichts regelmäßiges gesehen hat. Weiters hat das Freigeben von Speicher ewig lange gedauert (ca. 100 mal so lang wie normal), wenn man über 100MB in kurzer Zeit freigegeben hat und das in der selben Reihenfolge, wie man es angefordert hat usw.
3.) Richtige Low Level Benchmarks werden immer mit dem Assembler geschrieben, weil man nicht will, dass es möglichst schnell abläuft, sondern dass man genau einen spezifischen Befehl testet z.B. das dividieren und da ist es nicht sehr hilfreich, wenn der Compiler aus jedem x/2 ein x>>1 macht bzw. aus jedem "Durchmesser=Umfang/PI" ein "Durchmesser=Umfang*(1/PI)", wobei 1/PI eine Konstante ist, die der Compiler schon im Code ersetzt.