Gewinn durch HT/SMT

Welches der verfügbaren Programme (wie Cinebench oder Blender) vergleichbar wäre, kann ich nicht sagen, da ich deren Aufbau überhaupt nicht kenne. Wenn es durch HT/SMT nur mit bis zu 30% profitiert, dann ist es schon mal nicht vergleichbar.

Wie gesagt, ich werde eine vereinfachte Version vorbereiten (kann aber ein paar Tage dauern). Der interessierte Testteilnehmer müsste es dann selbst compilieren.

Die Auslastung im Taskmanager habe ich übrigens gar nicht geprüft. Das Programm gibt selbst aus, wieviele Strahlen es je Sekunde berechnet, so habe ich die Rechenleistung je Thread ermittelt.
 
Die Auslastung ist schon wichtig. Wenn 4 Threads die CPU nicht zu 50% auslasten, dann wäre klar warum 8 Threads so gut skalieren.
WinRAR wird gerne als Idealbeispiel für HT/SMT Skalierung genannt, aber es ist halt ein Ausreißer. Der Benchmark arbeitet z.B. immer mit 32 Threads und reagiert extrem auf Speicherlatenz. Der Benchmarkrekord wird von einem fast 4 Jahre alten i7-6950X mit 10 Kernen und Ringbus Architektur gehalten. WinRAR ist ein gutes Beispiel für eine Anwendung, die bestimmte CPU Architekturen bevorzugt.
 
  • Gefällt mir
Reaktionen: Baal Netbeck
MThom schrieb:
Welches der verfügbaren Programme (wie Cinebench oder Blender) vergleichbar wäre, kann ich nicht sagen, da ich deren Aufbau überhaupt nicht kenne. Wenn es durch HT/SMT nur mit bis zu 30% profitiert, dann ist es schon mal nicht vergleichbar.
Diese Programme nutzen schon ziemlich gut die CPU Kapazität.
Sie sind aber ziemlich perfekt darin ihre Arbeit auf viele Threads zu verteilen....jeder Thread kann kleine Teile des Bildes oder Videos unabhängig voneinander bearbeiten.

So kann HT/SMT noch einiges an Leistung gewinnen.

Um so mehr eine Anwendung durch Ramzugriffe limitiert ist, um so mehr kann ein Programm von HT/SMT profitieren.........ich habe Freitag Far Cry gekauft und mal im Benchmark die Threads limitiert.....Das Spiel ist CPU limitiert und nutzt nur grob 20-25% meiner 16 Thread CPU(2700X)....also grob vier Kerne.
logischerweise hat es keinen Unterschied gemacht, auf 12 Threads runter zu gehen.....und auch 4 Kerne mit 8 Threads liefen noch sehr gut....minimal langsamer allerdings.

Auch 4 Kerne ohne SMT gingen ziemlich gut, auch wenn es jetzt schlechtere minimum FPS gab.....2 Kerne waren dann aber grausig, weil das Spiel mehrfach stark gehangen hat....sich die Maus nur zäh bewegen ließ usw.

Aber 2Kerne +SMT waren fast so gut wie 4 echte Kerne.....Da gehe ich davon aus, dass die CPU fast immer mehr als zwei Takte auf Daten aus dem Ram warten musste und SMT daher in der Lage ist die Leistung fast zu verdoppeln.
 
So, hier ist es, auf das Nötigste abgespeckt:
C++:
// Programm zum Testen der Gleitkomma-Rechenleistung

// Benutzung:
// 1. mit Compiler nach Wahl übersetzen, möglichst mit allen Optimierungen
// 2. Programm starten und gewünschte Zahl der Threads eingeben
// 3. ein paar Sekunden laufen lassen, bis sich die Ausgabe
//    der Rechenleistung stabilisiert hat
// 4. ermittelte Leistung im Forum mitteilen

// Es kann nötig sein, unportable Programmteile zu ändern, vor allem den
// Multithreading-Teil. Die Alternative ist, den Multithreading-Teil
// ganz rauszuwerfen und das Programm mehrmals zu starten (mehrere Prozesse).
// Dazu einfach die Präprozessor-Anweisung unten ändern (#if 0 statt #if 1).

// Neben der ermittelten Leistung bitte mindestens folgende Angaben machen:
//  - Zahl der Threads/Prozesse
//  - Prozessortyp; ggfs. mit Angabe von Takt, TDP-Einstellung, Kühlung usw.
//  - verwendeter Compiler und dessen Parameter (vor allem Optimierung)

// Idealerweise testet man sowohl mit einem Thread als auch mit so vielen,
// wie Kerne vorhanden sind, als auch, wenn HT/SMT vorhanden ist,
// mit doppelt so vielen.



// Das Programm berechnet die Brechung von Lichtstrahlen an kugelförmigen
// Linsenoberflächen. Es gibt die Leistung je Thread aus,
// in millionen Strahlen je Sekunde. Ein Strahl wird der Reihe nach an 8
// Flächen gebrochen, was jeweils rund 45 Gleitkommaoperationen erfordert
// (siehe die Methode System::strahl, die einen Strahl durchrechnet).
// 1 mio Strahlen je Sekunde bedeutet also 360'000'000 Gleitkommaoperationen
// je Sekunde (und je Thread).

#include <iostream>
#include <time.h>
#include <math.h>

using std::cout;
using std::endl;

typedef unsigned long long U64;

class Rand { // fancy PRNG
    U64 a, b, c;
    static U64 ro(U64 a, U64 b) { return a << b | a >> (64-b); }
    void _rand() {
        a += c++;
        b ^= ro(a * 0x3874a66b48a6ae55, 32);
        a ^= ro(b * 0x406a27534e6ef1db, 31);
        b ^= ro(a * 0x194ba598311e3193, 23);
        a ^= ro(b * 0x65f61398d08ac887, 11);
    }
public:
    Rand(U64 s) : a(s), b(time(0)), c(0) {}
    double rand(bool sign) { // PRN in [0,1) or (-1,1)
        const double f = 0.5 / ((U64)1 << 63);
        _rand();
        return a * (sign && (b & 1) ? -f : f);
    }
};

//----------- der Optik-Teil -------------------------------------------------

const int N = 8; // Zahl der Flächen
const double T =  0.05; // Hauptstrahlsteigung
const double O =  2.0;  // Öffnungsradius
const double F = 50.0;  // Brennweite

struct Strahl {
    const double cx0, cy0;
    double y0, z0, y, z;
    Strahl() : cx0(1. / sqrt(T*T+1.)), cy0(T * cx0) {}
};

class System {
    double _k[N];         // Krümmungen der Flächen
    double _d[N];         // Abstände der Flächen (_d[N-1] ist Schnittweite)
    double _v[N], _v2[N]; // Verhältnis der Brechzahlen an den Flächen
    double _ep;           // Position der Eintrittspupille
public:
    System();
    double ep() const { return _ep; }
    void strahl(Strahl &) const;
};

System::System() {
    const double p[] = { // Daten des Linsensystems
        3.951754785, 5.725546581, 3.010066534, 8.537160394,
        7.609756865, 1.946574948, 4.66783622, 32.9165898,
        0.05455588075, 0.01527275136, 0.07943846653, 0.1098046131,
        -0.07929071194, -0.0381540381, -0.00233718517,
        1.520665213, 1.6171631, 1.663401941, 1.986201015, 25.71689858
    };
    int j = 0;
    for (int i=0 ; i<N ; ++i) _d[i] = p[j++];
    for (int i=0 ; i<N-1 ; ++i) _k[i] = p[j++];
    for (int i=1 ; i<N ; i+=2) _v[i] = p[j++];
    _ep = p[j];
    for (int i=0 ; i<N ; i+=2) _v[i] = 1. / _v[i+1];
    double a = 1, c = 0;
    for (int i=0 ; i<N-1 ; ++i) {
        c = c * _v[i] + a * _k[i] * (_v[i] - 1);
        a += _d[i] * c;
    }
    _k[N-1] = (1/F + c * _v[N-1]) / (a - a * _v[N-1]); // Krümmung letzte Fl.
    for (int i=0 ; i<N ; ++i) _v2[i] = _v[i] * _v[i];
}

void System::strahl(Strahl & str) const {
    // berechnet den Weg eines Strahls durch das System
    double x = 0;
    double y = str.y0;
    double z = str.z0;
    double A = str.cx0;
    double B = str.cy0;
    double C = 0;
    for (int i=0 ; i<N ; ++i) { // Schleife über die Linsenoberflächen
        double e = A*x + B*y + C*z;
        double h = _k[i] * (x*x + y*y + z*z - e*e) + 2 * (e*A - x);
        double R = A * A - h * _k[i];
        double E = sqrt(R);
        double d = h / (A + E) - e; // Abstand zum vorherigen Schnittpunkt
        x += d * A;
        y += d * B;
        z += d * C; // (x,y,z) == Schnittpunkt Strahl mit Fläche
        double g = sqrt(1 + _v2[i] * (R - 1)) - E * _v[i];
        double a = g * _k[i];
        A = A * _v[i] - x * a + g;
        B = B * _v[i] - y * a;
        C = C * _v[i] - z * a; // (A,B,C) == normierter Strahlvektor
        x -= _d[i];
    }
    x /= A;
    // Schnittpunkt mit Bildebene:
    str.y = y - x * B;
    str.z = z - x * C;
}

//----------------------------------------------------------------------------

// Jeder Thread bekommt ein Objekt der Klasse Thread_t (mit einem eigenen
// Objekt der Klasse System und einem eigenen Zufallsgenerator)
// und ruft darauf die Methode test() auf.

class Thread_t {
    const System sys;
    Rand rn;
    const time_t t0;
public:
    const int threads; // Zahl der Threads
    const int num;     // dieser Thread (0 bis threads-1)
    Thread_t(int thr, int n, time_t t) : rn(n), t0(t), threads(thr), num(n) {}
    void test();
};

void Thread_t::test() {
    const time_t interval = threads * (threads > 4 ? 1 : 2);
    time_t tn = t0 + num*interval/threads;

    const unsigned anz = 100;
    Strahl s;
    U64 c = 0;

    for (unsigned n=0 ;;)
    {
        // Berechnung:
        for (unsigned i=1 ; i <= anz ; ++i) {
            // erzeuge zufälligen Strahl
            s.z0 = O * rn.rand(false);
            s.y0 = O * rn.rand(true) - sys.ep() * T;
            // berechne Strahlweg:
            sys.strahl(s);
            double y = s.y - F * T;
            // prüfe auf Fehler:
            const double ylo = -983e-6;
            const double yhi = -963e-6;
            const double zlo = -5e-6;
            const double zhi = 13e-6;
            if (!(y > ylo && y < yhi && s.z > zlo && s.z < zhi)) {
                // Negation nicht entfernen, damit auch NaN erkannt wird
                cout << "error thread " << num << " after " << c+i;
                cout << " rays: " << y << ", " << s.z << endl;
                return;
            }
        }
        c += anz; // gesamte Zahl der Strahlen
        n += anz; // Zahl der Strahlen seit letzter Ausgabe

        // Ausgabe:
        const time_t t = time(0);
        if (t >= tn) {
            tn += interval; // Zeit für nächste Ausgabe
            if (threads > 1) {
                if (threads > 10 && num < 10) cout << ' ';
                if (threads > 100 && num < 100) cout << ' ';
                cout << "thread " << num << ": ";
            }
            cout.width(8);
            cout << std::left << n/(interval*1e6) << " Mrays/sec" << endl;
            //anz = ((unsigned)sqrt((double)n) + anz) / 2;
            //if (anz < 1) anz = 1;
            n = 0;
        }
    }
}

#if 1
#include <windows.h>

DWORD WINAPI thf(LPVOID p) {
    Thread_t *t = (Thread_t *)p;
    //cout << "thread " << t->num << " started" << endl;
    t->test();
    cout << " thread " << t->num << " ended" << endl;
    return 0;
}

void main() {
// assert(sizeof(U64) == 8);
    Thread_t *tv[256];
    int th;

    cout << "enter number of threads (1 ... 256):" << endl;
    std::cin >> th;
    if (th < 1 || th > 256) return;

    time_t t0 = time(0);
    for (int i=0 ; i<th ; ++i)
        tv[i] = new Thread_t(th, i, t0);

    for (int i=1 ; i<th ; ++i) {
        DWORD tID;
        if (CreateThread(0, 0, (LPTHREAD_START_ROUTINE)thf, (LPVOID)tv[i], 0, &tID) == NULL) {
            cout << "CreateThread error: " << GetLastError() << endl;
            abort();
        }
    }
    thf((LPVOID)tv[0]);
}

#else
// Alternative ohne Multithreading:
void main() {
    Thread_t th(1, 0, time(0));
    th.test();
}
#endif
Edit: habe #include <math.h> nachgetragen, was von VS 2013 nicht verlangt wurde, darum ist mir nicht aufgefallen, dass es fehlt.

Ergänzung ()

Meine bisherigen Ergebnisse:

übersetzt mit Visual Studio 2013 zu x64-Code
Optimierung /O2 und /Ot
Laufzeitbibliothek /MT (also nicht gegen DLL gelinkt)

Celeron J1900, Win 7
1 thread bis 4 threads: jeweils 1.18 Mrays/sec

i7 4790K (standard-settings, nur etwas undervoltet), Win 7
1 thread: 3.61 Mrays/sec
2 threads: 3.58
4 threads: 3.40
8 threads: 3.08 (das entspricht 181 % der Gesamtleistung von 4 threads)

i5 8250U (Lenovo ThinkPad L380), Win 10
1 thread: 3.03
2 threads: 3.03
4 threads: 2.6
8 threads: ca. 1.9 (schwankt etwas) (146 % der Gesamtleistung von 4 threads)

AMD V 120 (älterer MSI Laptop), Win 7
1 thread: 0.50


übersetzt mit Visual C++ 2008 zu x86 Code (32 bit)
Optimierung \Ox und \Ot
Bibliothek \MTd

Athlon II X2 255 (standard settings), Win XP
1 und 2 threads: 1.96 Mrays/sec

Pentium 4 2.6 GHz, Win XP
1 thread: 0.86
 
Zuletzt bearbeitet:
(Das stimmt hier leider nicht so ganz, mehr Analyse weiter unten :) -- mit aggressiveren Compiler-Flags geht fast der gesamte Stack-Traffic weg, und weil sqrt dann inlined wird, vermutlich auch alle Branches.)

Das macht quasi fast keine Rechnungen. Deine "heiße Schleife" hat ~20 Speicherzugriffe (auf den Stack) auf vlt. 10 ALU Operationen. Außerdem Branches. perf sagt z.B.:

Code:
    19.657.859.739      stalled-cycles-backend    #   91,38% backend cycles idle      (83,34%)
    19.341.759.742      instructions              #    0,90  insn per cycle   
                                                  #    1,02  stalled cycles per insn  (83,34%)
     1.247.018.107      branches                  #  234,063 M/sec                    (83,34%)

Sprich die CPU tut fast nichts außer in den Cache zu schreiben und aus dem Cache zu lesen. Du führst nicht mal eine Instruktion pro Takt durch im Schnitt. Ich habs jetzt auch mal mit perf record angeschaut, die "strahl" Funktion macht mehr movs vom/auf den Stack als sonst was. Da ist ein jmp drin, der kostet Dich 16% der Zeit, und der Rest wird im Frontend Instruktionen erzeugen aber im Backend nichts verarbeiten.

Mit HT/SMT werden diese Latenzen/Löcher in der Pipeline gestopft, weil da der andere Thread zum Zug kommt -> deswegen siehst Du da so viel bessere Leistung.

(BTW: main muss int zurückliefern, so kompiliert das z.B. nicht mit Clang :))
 
Zuletzt bearbeitet: (Nochmal ordentlich analysiert und Fehler gefunden :))
  • Gefällt mir
Reaktionen: Baal Netbeck
@ Anteru: vielen Dank für deine Untersuchung, aber ich verstehe das nur zum Teil. Das heißt wohl, die insgesamt schwache Leistung kommt daher, dass oft Zwischenergebnisse in den Cache ausgelagert werden müssen, weil nicht alle in die Register passen?

Mit welcher Hardware / Compiler und welchem Ergebnis (Mrays/sec) hast du übrigens getestet?

Weitere Ergebnisse von mir:
Unter Linux (Lubuntu) mit g++ übersetzt (Einzelthread-Variante, Optimierung -O3):
Celeron J1900:
1 bis 4 Prozesse: 1.29 Mrays/sec

Athlon II X2 255 (3.1 GHz):
1 und 2 Prozesse: 2.30 Mrays/sec

Insgesamt ist die Leistung eher enttäuschend. Auf dem 4790K zum Beispiel berechnet ein Thread grob 1 Milliarde Gleitkomma-Operationen je Sekunde, das bedeutet eine Operation auf vier Takte. Lässt sich das verbessern? Mit einem besser optimierenden Compiler? Oder sollte ich den Code anders schreiben? Wie würde ein Profi so etwas machen?
 
1 Milliarde Operationen ist sogar noch viel schlechter, weil SSE/AVX -- Du könntest 4/8 pro Takt machen, und wenn Du FMA ausnutzt, kriegst Du 8/16 pro Takt (jede Lane macht dann A*B+C). Ich habs mit Clang und auf einem TR 1950X probiert. (Für F32 -- für F64 ist das alles halbiert.)

Ein Profi würde sich mit einem Profiler dran setzen und da anfangen zu optimieren :) perf ist ein guter Start, da siehst Du was jede Code-Zeile kostet, und dann versuchst Du den Code geschickter zu schreiben. Hier würde es wahrscheinlich helfen, mehrere Strahlen gleichzeitig zu machen. Dann kann der Compiler das eventuell selber in Vektor-Code konvertieren, oder zumindest die Rechnungen besser schedulen.

EDIT: So, jetzt nochmal richtig kompiliert, und der Call zu sqrt ist raus (... hab wohl zu wenig -O angehabt ... Schande über mein Haupt):

perf stat sagt nun:

Code:
29.494.746.806      cycles                    #    3,687 GHz                      (83,30%)
     6.149.915      stalled-cycles-frontend   #    0,02% frontend cycles idle     (83,31%)
27.898.016.641      stalled-cycles-backend    #   94,59% backend cycles idle      (83,34%)
15.918.274.517      instructions              #    0,54  insn per cycle

So sieht das hier aus (mit -ffast-math -march=native -O3), und da gefällt mir die innerste Schleife schon ganz gut. Das macht zwar nur eine Lane (alles ist sd, "scalar double"), aber da passiert nicht mehr viel Traffic auf dem Stack. So, und nun müssen wir beim Agner Fog schauen, und da lernen wir: vsqrt braucht 10-15 cycles, und läuft nur auf Port 3 der ALU. Sprich: P0, 1, 2 und sind idle, während der an der Wurzel rödelt. In der Zeit können die anderen Ports arbeiten, und da hilft eben SMT um dort Instruktionen reinzubekommen. add geht btw auf Port 2,3, und mul auf Port 0,1, deswegen sind die im Profile so billig. div geht auch auf Port 3, von daher würde es mich nicht wundern, wenn die FPU Ports einfach nur sehr ungleichmäßig ausgelastet sind und SMT dafür sorgt, dass Du jeden Takt an einen der 4 Ports was schickst.

Das ganz läuft btw mit ~4.5 MRay/sec pro Thread auf meinem TR1950X mit 1-32 Threads.
1577028940773.png
 
Zuletzt bearbeitet:
Zurück
Oben