32bit Anwendungen aufgrund von kürzeren Speicheradressen schneller!?

Nai schrieb:
Gesamtes Testprogramm dafür bitte :)

Ich habe es noch ein bischen ergänzt. Neuer Vergleich:
32 Bit
Code:
int8: 		3651
int16: 		3822
int32: 		3853
int64: 		4056
float: 		5226
double: 	17769
long double: 	17908
64 Bit:
Code:
int8: 		748
int16: 		749
int32: 		733
int64: 		749
float: 		733
double: 	733
long double: 	749
Werte in Millisekunden.

Source:
Code:
#pragma hdrstop
#pragma argsused

#ifdef _WIN32
#include <tchar.h>
#else
  typedef char _TCHAR;
  #define _tmain main
#endif

#include <iostream>

const int ARRAY_LENGTH = 50000;
using namespace std;

void doInt8()
{
	__int8 array[ARRAY_LENGTH];
	double start = clock();

	cout << "int8: ";

	for (int j = 1; j < ARRAY_LENGTH; j++) {
		array[0] = j;
		for (int i = 1; i < ARRAY_LENGTH; i++) {
			array[i] = array[i - 1]++;
		}
	}

	cout << "\t\t" << clock() - start << endl;
}

void doInt16()
{
	__int16 array[ARRAY_LENGTH];
	double start = clock();

	cout << "int16: ";

	for (int j = 1; j < ARRAY_LENGTH; j++) {
		array[0] = j;
		for (int i = 1; i < ARRAY_LENGTH; i++) {
			array[i] = array[i - 1]++;
		}
	}

	cout << "\t\t" << clock() - start << endl;
}

void doInt32()
{
	__int32 array[ARRAY_LENGTH];
	double start = clock();

	cout << "int32: ";

	for (int j = 1; j < ARRAY_LENGTH; j++) {
		array[0] = j;
		for (int i = 1; i < ARRAY_LENGTH; i++) {
			array[i] = array[i - 1]++;
		}
	}

	cout << "\t\t" << clock() - start << endl;
}

void doInt64()
{
	__int64 array[ARRAY_LENGTH];
	double start = clock();

	cout << "int64: ";

	for (int j = 1; j < ARRAY_LENGTH; j++) {
		array[0] = j;
		for (int i = 1; i < ARRAY_LENGTH; i++) {
			array[i] = array[i - 1]++;
		}
	}

	cout << "\t\t" << clock() - start << endl;
}

void doFloat()
{
	float array[ARRAY_LENGTH];
	double start = clock();

	cout << "float: ";

	for (int j = 1; j < ARRAY_LENGTH; j++) {
		array[0] = j;
		for (int i = 1; i < ARRAY_LENGTH; i++) {
			array[i] = array[i - 1]++;
		}
	}

	cout << "\t\t" << clock() - start << endl;
}

void doDouble()
{
	double array[ARRAY_LENGTH];
	double start = clock();

	cout << "double: ";

	for (int j = 1; j < ARRAY_LENGTH; j++) {
		array[0] = j;
		for (int i = 1; i < ARRAY_LENGTH; i++) {
			array[i] = array[i - 1]++;
		}
	}

	cout << "\t" << clock() - start << endl;
}

void doLongDouble()
{
	long double array[ARRAY_LENGTH];
	double start = clock();

	cout << "long double: ";

	for (int j = 1; j < ARRAY_LENGTH; j++) {
		array[0] = j;
		for (int i = 1; i < ARRAY_LENGTH; i++) {
			array[i] = array[i - 1]++;
		}
	}

	cout << "\t" << clock() - start << endl;
}

int _tmain(int argc, _TCHAR* argv[])
{
	doInt8();
	doInt16();
	doInt32();
	doInt64();
	doFloat();
	doDouble();
	doLongDouble();

	system("PAUSE");
	return 0;
}
Compiliert im Anhang.
 

Anhänge

Ich habe auch mal schnell ein Testprogramm gebastelt (eher hingerotzt :kotz:).

Als 32 Bit Version (auf 64 Bit Host) sind 32 Bit Operationen (habe hier auf arithmetische Funktionen getestet) etwas langsamer als in der nativen 64 Bit Version. Bei float/doubles gibt es kaum einen Unterschied.

Der CPU Type spielt natürlich auch eine Rolle ...:

Wie macht man einen Spoiler???

Notebook: Core I5 3320M (2.6 GHz)

Code:
Thread Count = 4 target = 64 Bit
Please Wait ...
addSub32         single   379,9, multi  1192,2 (MegaIterations/s) factor 3,14
addSub64         single   378,7, multi  1164,5 (MegaIterations/s) factor 3,07
addSubMul32      single   255,1, multi   958,5 (MegaIterations/s) factor 3,76
addSubMul64      single   258,1, multi   926,1 (MegaIterations/s) factor 3,59
div32            single   136,1, multi   367,5 (MegaIterations/s) factor 2,7
div64            single    47,8, multi   116,9 (MegaIterations/s) factor 2,45
floatAddSubMul   single   488,7, multi  1196,1 (MegaIterations/s) factor 2,45
doubleAddSubMul  single   500,7, multi  1196,1 (MegaIterations/s) factor 2,39
doubleComplex    single    19,8, multi    61,6 (MegaIterations/s) factor 3,12
Finish ... Press any key

Code:
Thread Count = 4 target = 32 Bit
Please Wait ...
addSub32         single   295,1, multi   862,7 (MegaIterations/s) factor 2,92
addSub64         single   110,2, multi   331,1 (MegaIterations/s) factor 3,01
addSubMul32      single   237,4, multi   836,0 (MegaIterations/s) factor 3,52
addSubMul64      single    47,3, multi   104,3 (MegaIterations/s) factor 2,21
div32            single   117,9, multi   308,4 (MegaIterations/s) factor 2,62
div64            single    56,7, multi   114,2 (MegaIterations/s) factor 2,01
floatAddSubMul   single   500,7, multi  1016,0 (MegaIterations/s) factor 2,03
doubleAddSubMul  single   499,0, multi   987,6 (MegaIterations/s) factor 1,98
doubleComplex    single    18,2, multi    53,7 (MegaIterations/s) factor 2,94
Finish ... Press any key

Server: AMD A4-5300 (3.5 GHz)

Code:
Thread Count = 2 target = 64 Bit
Please Wait ...
addSub32         single   493,9, multi   961,6 (MegaIterations/s) factor 1,95
addSub64         single   500,5, multi   977,4 (MegaIterations/s) factor 1,95
addSubMul32      single   217,4, multi   407,4 (MegaIterations/s) factor 1,87
addSubMul64      single   148,7, multi   288,2 (MegaIterations/s) factor 1,94
div32            single   118,7, multi   238,1 (MegaIterations/s) factor 2,01
div64            single   111,7, multi   222,3 (MegaIterations/s) factor 1,99
floatAddSubMul   single   353,1, multi   687,9 (MegaIterations/s) factor 1,95
doubleAddSubMul  single   352,2, multi   681,6 (MegaIterations/s) factor 1,94
doubleComplex    single    17,5, multi    23,2 (MegaIterations/s) factor 1,33
Finish ... Press any key


Code:
Thread Count = 2 target = 32 Bit
Please Wait ...
addSub32         single   339,3, multi   620,3 (MegaIterations/s) factor 1,83
addSub64         single    92,1, multi   174,2 (MegaIterations/s) factor 1,89
addSubMul32      single   197,8, multi   369,4 (MegaIterations/s) factor 1,87
addSubMul64      single    40,0, multi    64,7 (MegaIterations/s) factor 1,62
div32            single    96,5, multi   192,0 (MegaIterations/s) factor 1,99
div64            single    60,8, multi    85,2 (MegaIterations/s) factor 1,4
floatAddSubMul   single   328,0, multi   421,1 (MegaIterations/s) factor 1,28
doubleAddSubMul  single   323,5, multi   420,4 (MegaIterations/s) factor 1,3
doubleComplex    single    13,6, multi    18,0 (MegaIterations/s) factor 1,32
Finish ... Press any key
 

Anhänge

Zuletzt bearbeitet:
Q6600 @ 3,4 GHz

Code:
Thread Count = 4 target = 32 Bit
Please Wait ...
addSub32         single   298,8, multi  1112,8 (MegaIterations/s) factor 3,72
addSub64         single   127,0, multi   464,8 (MegaIterations/s) factor 3,66
addSubMul32      single   198,7, multi   727,9 (MegaIterations/s) factor 3,66
addSubMul64      single    47,8, multi   182,7 (MegaIterations/s) factor 3,82
div32            single   152,6, multi   577,4 (MegaIterations/s) factor 3,78
div64            single    65,1, multi   232,0 (MegaIterations/s) factor 3,57
floatAddSubMul   single   422,7, multi  1606,9 (MegaIterations/s) factor 3,8
doubleAddSubMul  single   422,4, multi  1629,3 (MegaIterations/s) factor 3,86
doubleComplex    single    15,7, multi    53,3 (MegaIterations/s) factor 3,4
Finish ... Press any key
Code:
Thread Count = 4 target = 64 Bit
Please Wait ...
addSub32         single   495,5, multi  1800,6 (MegaIterations/s) factor 3,63
addSub64         single   537,8, multi  1989,8 (MegaIterations/s) factor 3,7
addSubMul32      single   267,0, multi  1000,4 (MegaIterations/s) factor 3,75
addSubMul64      single   153,7, multi   587,1 (MegaIterations/s) factor 3,82
div32            single   180,2, multi   662,3 (MegaIterations/s) factor 3,67
div64            single    33,9, multi   126,1 (MegaIterations/s) factor 3,72
floatAddSubMul   single   365,5, multi  1341,9 (MegaIterations/s) factor 3,67
doubleAddSubMul  single   373,5, multi  1333,5 (MegaIterations/s) factor 3,57
doubleComplex    single    20,7, multi    76,8 (MegaIterations/s) factor 3,7
Finish ... Press any key
Bei addSub64 und addSubMul64 habe ich bei 64 Bit mehr als die 3 bis 4 fache Geschwindigkeit. Bei floatAddSubMul und doubleAddSubMul ist 64 Bit etwas langsamer, bei div64 habe ich sogar nur noch etwa die halbe Geschwindigkeit.
Das kommt mir schon rigendwie seltsam vor.

Ein Spoiler funktioniert so:
[SPO.ILER]Text im Spoiler[/SPO.ILER]
 
Bei 64 Bit Operation im 32 Bit Modus muss man auch mehre Befehle ausführen (z.B. 2 Adds mit Carry und nicht nur ein Add wie bei 64 Bit). Deswegen sind 64 Bit arithmetische Instruktionen (mal von FPU abgesehen) im 32 Bit Modus immer um einige Faktoren langsamer.

Warum die Float/Doubles und der div64 bei dir unter 64Bit langsamer sind, keine Ahnung, vielleicht ist mein Test Mist :lol:

P.s.: Die single Thread performance von meinem A4 ist gar nicht so schlecht (fast gleich zu deinem Q6600) :rolleyes:
 
Zuletzt bearbeitet:
Ich hatte spaßeshalber mal in C# den Versuch gemacht. Die Arrays aber auch die Schleifenzähler als entsprechende int-Datentypen deklariert und einmal als x86 und einmal als x64 kompiliert. Bei mir war x64 deutlich langsamer, was zeigt, dass die Methodik nicht Narrensicher ist. Ich wäre also vorsichtig damit, irgendwelche Aussagen davon abzuleiten.
 
Ich habe meine Anwendung jetzt auch mal nach C# übertragen. Bei int64, float und double liegt die 64 Bit Anwendung klar vorn. Ansonsten steht 32 Bit besser da. C# hat es offensichtlich nicht so mit 64 Bit.

32 Bit
Code:
sbyte:          2909
int16:          3658
int32:          3626
int64:          6097
float:          4563
double:         4655
decimal:        67778
64 Bit
Code:
sbyte:          3577
int16:          3652
int32:          3700
int64:          3856
float:          3827
double:         4048
decimal:        81151
 
Ich denke mal C# ist nun wirklich die am schlechtesten geeignete Programmiersprache um dies zu testen, da diese ja nach .NET kompiliert wird, was ja so eine Art "Zwischensprache" ist. Hier testet man also eher die 32- bzw. 64-Bit Version der .NET VM als wirklichen Maschinencode.
 
Wenn ich mal kurz anmerken dürfte:
ob 32 oder 64bit macht über die Performance eigentlich keine Aussage. Entscheidend ist die Technologie, die intern eingesetzt wird. Also tatsächlich wie intern eine Operation aufgelöst wird.

Ein Problem wäre nur, wenn die Anwendung eigentlich einen 64bit-Wert berechnen muss und die Platform intern nur 32bit kann. Dann muss im schlechtesten Fall noch einer weitere Operation ausgeführt werden, wenn es in der niederwertigen Addition einen Overflow gibt. Das war es aber dann auch schon.

Auf einer 64bit-Maschine dürfte es kaum einen Unterschied machen, ob die Anwendung für 32 oder 64bit kompiliert wurde, solange die gleichen Operationen verwendet werden. In der CPU selber sind sicher nicht 1mal 32bit und 1mal 64bit verbaut sondern eben nur 64bit breite Register (8bit ist der untere teil von 16bit ... von 32 bit ... von 64bit. In Assembler auch als AL, AX, EAX, RAX bezeichnet).

Instruktionen sollten dadurch also nicht länger werden( Register EAX ist einfach nur die niedrigere Hälfte von RAX und kein neues Register). Deshalb sollte (meiner Meinung nach) auch kein bemerkbarer Overhead entstehen, wenn es um die Ausführung der Instruktionen geht. Meine Vermutung ist, dass der Rambedarf steigt, da Zeiger nun eben einfach länger sind. Bei den Caches wird man wohl vorher bereits Anpassungen gemacht haben, damit überhaupt 64bit-Datenworte hineinpassen. Warum das auf Kosten der Anzahl an Cachelines gehen sollte leuchtet mir jetzt nicht ganz ein. Die Cacheline-Größen sind auch nochmal gewachsen. Ob das allerdings alles wett macht weiß ich nicht.

Worauf ich eigentlich hinaus will: 32bit ist nicht von Haus aus schneller oder langsamer als 64bit, sondern es kommt auf die zugrunde liegende Technologie (und Architektur) an.
 
64 Bit Pointer belegen natürlich mehr Speicher als 32 Bit Pointer und die Caches wachsen nicht aus wundersame Weise, wenn man 64 Bit statt 32 Bit Code ausführt. Wenn man also jede Menge Pointer hat bspw Bäume oder doppelt verkettete Listen (vorwärts + rückwärts Pointer je 32 Bit größer, macht bei 8000 Elementen immerhin 64KByte zusätzlicher Speicherbedarf. Zur Erinnerung: 64KByte L1 Cache hat Intels neuster, der Haswell, und 8000 ist nun auch keine schrecklich große Zahl) hat, die man mehrfach besucht und aufgrund des Speichermehrbedarfes aus dem Cache gefallen sind, können natürlich langsamer sein, als mit 32Bit Pointern.

Sollte man jetzt grundsätzlich Anwendungen als 32 Bit kompilieren, wenn man weniger als 2GB Speicher braucht?
Auf gar keinen Fall! Mit x86-64 wurden auch zusätzliche Register eingeführt, sowohl von den allgemeinen Registern, als auch mehr SSE Register. Je nach Anwendungsfall spielen die größeren Pointer keine Rolle, aber die zusätzlichen Register können einen Performance Vorteil bringen. Auch sind, insbesondere unter Linux, Funktionsaufrufe möglicherweise schneller, weil die vielen zusätzlichen Register genutzt werden, um Parameter per Register weiterzugeben, statt auf den Stack zu pushen. Wenn man also bspw. test(4.0, 5.3) aufruft, werden die beiden doubles in SSE Registern weitergegeben, statt auf den Stack abzuladen.

Unter Linux hatte es übrigens mal eine Initiative gegeben, die 64 Bit Vorteile mit 32 Bit Pointen zu kombinieren: http://www.heise.de/open/artikel/Ke...ht-Nachteile-des-64-Bit-Betriebs-1341264.html
Hab ich aber lange nichts mehr von gehört, scheint irgendwie im Sande verlaufen zu sein. (das was chithanh auch bereits erwähnte)

Netter Nebeneffekt: Es gab keine x86-64 Prozessoren ohne SSE, ganz im Gegensatz zu x86 Prozessoren. Hat meinen einen sehr konservativen 32 Bit Compiler, kann es sein, das der ohne zusätzliche Einstellungen noch Code unnötigerweise generiert, der auch noch auf dem Pentium 1 läuft. Gei 64 Bit sind vermutliche viele Compilerhersteller dazu übergegangen einfach immer mindestens SSE Code zu generieren. Aus diesem Grund sind viele Linux Distributionen in der 64 Bit Variante schneller, einfach weil sie ständig SSE benutzen.

An der Stelle nämlich gleich einen Rüffel für S.Karas Testprogamm: Hier verwendet das 32Bit Kompilat tatsächlich nicht SSE, beim 64 Bit Kompilat hingegen schon, sogar der Autovektorisierer war aktiv (d.h. bspw wenn bei floats (32Bit) vier davon in die 128 Bit Register passen, macht der dies auch und berechnet dann das Ergebnis in einem Schritt. Speedup ~4 fach!). Damit wird hier die x87 FPU gegen 128Bit Vektoreinheiten verglichen, 64 Bit oder nicht, spielt bei dem Testprogramm somit überhaupt keine Rolle. Für einen fairen Vergleich müsste man auch dem 32Bit Kompilat den Autovektorisierer/SSE zugestehen. (Für Linuxer/cygwin Nutzer: Wenn ihr die Existenz von SSE/AVX Code prüfen wollt: objdump -d name_der_anwendung | grep "xmm" bzw. "ymm" bei AVX, funktioniert auch bei Windows Anwendungen)

Auch wenn es ursprünglich nur um 64Bit Pointer ging, aber hier dann irgendwann es auch überging zur Verarbeitung von 64Bit Zahlen:
Schon MMX (Pentium 1 MMX gabs mit den Frequenzen 166MHz und 200MHz, ist also schon eine Weile her) hatte 64Bit breite Register... Wer also wirklich numerische Anwendungen hatte, verwendete jeher einfach die Vektorregister. Bei der Einführung von x86-64 ging es also nicht wirklich ums Verarbeiten von 64 Bit Zahlen, sondern primär um den größeren Adressbereich.
 
Danke für den Quelltext :)

Was mich etwas an Karas Programm wundert: Wieso hat der Compiler dort nicht per Dead-Code-Removal die gesamten Problemberechnungen, die kein I-O verursacht haben, rausoptimiert ? Normalerweise sollte das afaik standard sein . . . .
 
disassembler schrieb:
64 Bit Pointer belegen natürlich mehr Speicher als 32 Bit Pointer und die Caches wachsen nicht aus wundersame Weise, wenn man 64 Bit statt 32 Bit Code ausführt. Wenn man also jede Menge Pointer hat bspw Bäume oder doppelt verkettete Listen (vorwärts + rückwärts Pointer je 32 Bit größer, macht bei 8000 Elementen immerhin 64KByte zusätzlicher Speicherbedarf. Zur Erinnerung: 64KByte L1 Cache hat Intels neuster, der Haswell, und 8000 ist nun auch keine schrecklich große Zahl) hat, die man mehrfach besucht und aufgrund des Speichermehrbedarfes aus dem Cache gefallen sind, können natürlich langsamer sein, als mit 32Bit Pointern.

Ich meinte auch nicht, dass der Cache auf wundersame Weise größer wird, sondern, dass er immer größer wird. Dadurch relativieren sich die 32bit extra pro Pointer doch etwas. Und einen Baum/Liste kann man auch anders implementieren. Wenn ich tatsächlich so viele Elemente habe, dass sie meinen Cache sprengen würde ich mir Gedanken drüber machen, ob der Overhead durch die Pointer nicht umgangen werden kann und das ist ja nicht ganz so schwer. Abgesehen davon gehe ich mal davon aus, dass die Daten größer sind als die Pointer und damit sieht die Sache sowieso wieder anders aus. Überhaupt sollten die Pointer nicht unbedingt im Speicher überall zwischen den eigentlichen Daten liegen sondern lieber separat gespeichert sein. Aber das ist nur meine Meinung.

Den Rest deines Posts finde ich sehr informativ. Dass Standardmäßig SSE genutzt wird habe ich mir zwar gedacht aber bisher nicht gewusst. Danke dafür :)
 
Noch etwas Senf, von mir, dazu:

Unter x86 + AMD64, ist die Parameterübergabe (Register und/oder Stack) auch vom Compiler abhängig (siehe http://de.wikipedia.org/wiki/Aufrufkonvention ganz unten).

Bei anderen Architekturen ist das normal, bei MIPS z.B. haben die Register noch einen „Common“ Namen. Z.b.
r2,r3 = v0,v1 => Funktionsrückgabe
r4-r7 = Funktionsargument
r8-r15 = temporärere Register
….
Bei SPARC gibt es z.B. Registerwindows, d.h. man addiert/subtrahiert (bei Sprung zum Unterprogramm bzw. zurück) intern auf einen großen Registerbereich einen Offset und sieht dann immer nur einen Teil der Registersets. Man kann hier also, teilweise, auf dem Stack verzichten (Übergabe, Rückgabe, temporär).

Beim EPIC- Design kann man für so etwas auch Register-Rotation verwenden, ist aber in erster Linie gedacht um Latenzen zu vermeiden.
 
Zurück
Oben