Wie bei so vielen Dingen in der Hochleistungsinformatik ist auch hier der Schlüssel zum Verständnis der Leistung das Verständnis der Speichernutzung.
Wenn Sie einen Thread verwenden, um eine Multiplikation durchzuführen, müssen Sie für diesen Thread zwei Daten aus dem Speicher holen, sie multiplizieren und dann eine logarithmische Anzahl von Additionen durchführen. Das sind drei Speicherzugriffe für eine Multiplikation, eine Addition und ein Bit - die arithmetische Intensität ist sehr gering. Die gute Nachricht ist, dass es auf diese Weise viele, viele Threads mit Aufgaben gibt, von denen jede nur ein winziges Stückchen Speicher/Register benötigt, was gut für die Auslastung ist; aber das Verhältnis von Speicherzugriff zu Arbeit ist schlecht.
Der einfache Ansatz, bei dem ein Thread ein Punktprodukt ausführt, hat die gleiche Art von Problem - jede Multiplikation erfordert zwei Speicherzugriffe zum Laden. Die gute Nachricht ist, dass es nur einen Speicherzugriff auf den globalen Speicher für das gesamte Punktprodukt gibt, und Sie vermeiden die binäre Reduktion, die nicht so gut skaliert und eine Menge Synchronisation erfordert; die Kehrseite ist, dass es jetzt viel weniger Threads gibt, was zumindest Ihr (b) Ansatz für Sie getan hat.
Jetzt wissen Sie, dass es eine Möglichkeit geben sollte, mehr Operationen pro Speicherzugriff als diese zu tun; für quadratische NxN-Matrizen gibt es N^3 Arbeit, um die Multiplikation zu tun, aber nur 3xN^2 Elemente - so sollten Sie in der Lage sein, einen Weg zu finden, weit mehr als 1 Berechnung pro 2ish Speicherzugriffe zu tun.
Der Ansatz, der im CUDA SDK verfolgt wird, ist der beste Weg - die Matrizen werden in Kacheln unterteilt, und Ihr Ansatz (b) - ein Thread pro Ausgabeelement - wird verwendet. Der Schlüssel liegt jedoch darin, wie die Threads angeordnet sind. Indem man ganze kleine Teilmatrizen aus dem langsamen globalen Speicher in den gemeinsamen Speicher zieht und von dort aus Berechnungen durchführt, ist es möglich, viele Multiplikationen und Additionen mit jeder Zahl durchzuführen, die man aus dem Speicher eingelesen hat. Dieser Ansatz ist in vielen Anwendungen am erfolgreichsten, da die Datenbeschaffung - sei es über ein Netzwerk, aus dem Hauptspeicher einer CPU oder aus dem Off-Chip-Speicher einer GPU - oft viel länger dauert als die Verarbeitung der Daten.
Es gibt Dokumente auf den CUDA-Seiten von NVidia (insb. http://developer.nvidia.com/object/cuda_training.html ), die ihr SDK-Beispiel sehr gut beschreiben.