Berechnung abladen erlaubt

Offload-Programmierung: Applikationscode beschleunigen
Kommentare

Mit dem Intel Xeon Phi Coprocessor setzt Intel auf Beschleunigung von Applikationscode durch eine große Anzahl an einfachen Rechenkernen mit sehr breiten Vektorregistern. Die Rechenkerne entstammen der Intel-Architektur und sind wie ihre großen Brüder der Xeon-Familie vollprogrammierbar. Somit können Programmierer auf die üblichen Programmiersprachen (C/C++, Fortran, Python usw.) und -werkzeuge (Intel Composer XE, GCC, GDB usw.) zurückgreifen. Obwohl Anpassungen des Codes für optimale Performance wie bei jeder Portierung auf eine neue Plattform nötig sind, können diese inkrementell und ohne Nutzung von speziellen Sprachen umgesetzt werden.

In der größten Ausbaustufe (Modellnummer 7120A/P) bietet eine PCIe-Karte 61 Rechenkerne mit je 32 Vektorregistern zu 512 Bit. Das entspricht acht doppelgenauen Fließkommazahlen; bei einfacher Genauigkeit verdoppelt sich die Anzahl entsprechend. Jeder Rechenkern kann vier Hardwarethreads im Wechsel verarbeiten. Somit stehen einer Applikation sage und schreibe 244 logische Rechenkerne zur Verfügung, die sinnvoll genutzt werden wollen. Für weitere Informationen zur Architektur des Coprozessors sei an dieser Stelle auf folgenden Link verwiesen.

Aus Sicht des Programmierers kann eine Coprozessorkarte entweder als eigenständiger Rechner betrieben werden („native“) oder mittels Offload als Coprozessor des Hostsystems dienen. Für die native Ausführung stellt die Karte alle nötigen Funktionen, ausgehend von einem vollständigen Linux-Kern und dessen typischen Schnittstellen bis hin zu einer kompletten Integration in ein TCP/IP- oder Infiniband-Netzwerk zur Verfügung. Somit können Anwendungen direkt auf dem Coprozessor gestartet werden und mit der Außenwelt und/oder anderen Coprozessoren in Kontakt treten.

Beim Offload-Modell verbleibt die Kontrolle über die Ausführung weitestgehend bei dem Teil der Anwendung, der auf dem Host ausgeführt wird. Von Zeit zu Zeit überträgt sie Daten und Kontrolle vom Hostsystem auf den Coprozessor, um die Ausführung zu beschleunigen. Massive parallele Algorithmen mit großem Potenzial für Vektorisierung sind geeignete Kandidaten für Offloading. Coderegionen mit überwiegend sequenziellem Anteil oder schlechter Vektorisierbarkeit können auf dem Host verbleiben.

Hallo Offload-Welt!

Es ist praktisch schon fast Tradition, mit einem „Hallo Welt!“-Programm zu starten. Listing 1 und 2 zeigen die zwei unterschiedlichen Ansätze, die von Intel Corporation für C/C++-Programme unterstützt werden. Der C-Code in Listing 1 verwendet die OpenMP-Konstrukte von OpenMP* 4.0, um Offload-Code zu markieren. Alles zwischen der öffnenden und schließenden Klammer nach #pragma omp target wird vom Compiler so übersetzt, dass das Codefragment sowohl auf dem Host als auch auf dem Coprozessor ausgeführt werden kann. Um das Beispiel gleich von Beginn etwas komplexer zu gestalten, ist die Bildschirmausgabe in eine eigene Funktion ausgelagert. Diese muss ebenfalls für die Ausführung auf dem Coprozessor übersetzt werden. Das übernimmt das vorangestellte die Direktiven #pragma omp declare target und #pragma omp end declare target. Alle davon eingeschlossenen Funktionen sind sowohl auf dem Host als auch auf dem Coprozessor für Aufrufe auf Offload-Regionen verfügbar.

Listing 1


#include <stdio.h>

 

/* Markiere die Funktion hello als Offload-Funktion */

#pragma omp declare target

void hello(void) {

printf("Hallo Offload-Welt\n");

}

#pragma omp end declare target

 

int main(int argc, char* argv[]) {

#pragma omp target

{

/* Alles in diesem Block wird auf dem Coprozessor ausgeführt */

hello();

}

return 0;

}

Listing 2 verfolgt einen zweiten Offload-Ansatz über die Spracherweiterungen von Intel Cilk Plus. Diese definieren zusätzlich zu den Schlüsselwörtern für Multi-Threading auch weitere Schlüsselwörter für Offloading. Ein Funktionsaufruf in Cilk Plus kann mit _Cilk_offload versehen werden, um den Funktionsaufruf auf dem Coprozessor auszuführen. Wird eine Funktionsdefinition mit _Cilk_shared gekennzeichnet, erzeugt der Compiler wie in Listing 1 eine Version für die Ausführung auf dem Coprozessor.

Listing 2


#include <stdio.h>

 

/* Markiere die Funktion hello als Offload-Funktion */

_Cilk_shared

void hello(void) {

printf("Hallo Offload-Welt\n");

}

 

int main(int argc, char* argv[]) {

/* Der Funktionsaufruf für hello() wird auf dem Coprocessor ausgeführt */

_Cilk_offload hello();

return 0;

}

An dieser Stelle ist es wichtig zu verstehen, dass jede der vorgestellten Offload-Sprachen im Gegensatz zu OpenCL* das Offloading und die Parallelisierung als getrennte Aspekte behandelt. OpenCL parallelisiert einen Kernel automatisch, sobald ein Offload stattfindet. Im Gegensatz dazu wird bei den vorgestellten Ansätzen Code rein sequentiell ausgeführt, wenn die Kontrolle vom Host zum Coprozessor übergeht. Programmierer müssen explizit Sorge dafür tragen, dass Threads gestartet und mit Arbeit versorgt werden. Das klingt zunächst wie eine gravierende Einschränkung und erhöht scheinbar den Aufwand auf Seiten der Programmierer. Es erlaubt jedoch die Wahl eines geeigneten Programmiermodells für Multi-Threading, sei es Intel Threading Buildings, OpenMP, Intel Cilk Plus oder schlicht POSIX*-Threads. Ebenso können sehr einfach bereits parallele Funktionen aus Bibliotheken (bspw. Intel Math Kernel Library) aufgerufen werden.

Intermezzo: Zeigt her euren Offload!

Abbildung 1 zeigt den schematischen Ablauf für die Suche nach Offload-Kandidaten. Alles beginnt mit der Auswahl eines geeigneten Benchmarks, der das Verhalten der Applikation möglichst realitätsnah abbildet und die gleichen Codeteile der Applikation in Anspruch nimmt. Wichtig ist, dass der Benchmark dabei nicht allzu lange ausgeführt werden muss; zwei bis maximal fünfzehn Minuten sind optimal.

Abb. 1: Analyseprozess zur Ermittlung von Coderegionen für Offloading

Abb. 1: Analyseprozess zur Ermittlung von Coderegionen für Offloading

Mit diesem Benchmark startet man eine so genannte Hotspotanalyse in Intel VTune Amplifier XE, um die heißen, sprich aus Sicht der verbrauchten Rechenzeit relevanten, Codestellen zu lokalisieren. Diese Hotspots sind potenzielle Kandidaten für die Ausführung auf dem Coprozessor, da sie die meiste Rechenzeit verbrauchen und somit den größten Gewinn durch Beschleunigung des Codes versprechen.

Der nächste Schritt ist die Analyse der Funktionsaufrufgraphen, oder kurz Call-Tree. Ausgehend von den Hotspots in der Applikation ermitteln wir so gemeinsame Unterbäume im Aufrufgraphen, die mögliche Ankerpunkte für Datentransfers oder Offloading darstellen. Häufig zeigt sich, dass mehrere Hotspots innerhalb der gleichen die Funktion aufgerufen werden und möglicherweise auch auf den gleichen Daten arbeiten. In solchen Fällen bietet es sich an, entweder im Aufrufer Datentransfers durchzuführen oder den gesamten Aufrufer als größere Offload-Region zu markieren.

Die anschließende Schleifenanalyse ermittelt, ob ein Offload-Kandidat auch wirklich genügend Parallelität und Vektorisierung mitbringt. Hierfür stehen mehrere Informationsquellen zur Verfügung. Einerseits hilft die Erfahrung des Programmierers bei der Codeanalyse in Bezug auf Parallelisierbarkeit. Andererseits gibt es aber auch Werkzeuge wie z. B. Intel Advisor XE oder auch die Berichtsfunktion des Intel-Compilers, die Codefragmente auf Parallelität abprüfen können. Wichtig ist, für Schleifen alle Datenabhängigkeiten zu ermitteln, die Parallelität und Vektorisierung verhindern. Weiterhin ist es wichtig, die minimale, maximale und durchschnittliche Anzahl der Schleifen zu kennen.

Sobald die Analysen abgeschlossen sind, kann die wirkliche Arbeit am Code beginnen. Jetzt ist es an der Zeit, entsprechende Direktiven (OpenMP) oder Schlüsselwörter (Intel Cilk Plus) einzuführen und Multi-Threading hinzuzufügen. Als letzten Schritt können bei Bedarf noch Optimierungen am Code vorgenommen werden, sodass der Code in den Offload-Regionen besser auf den Coprozessor abgestimmt wird. Aufgrund der Nähe zur Xeon-Familie bieten sich hier die üblichen Optimierungstechniken an.

Datenmanagement

Natürlich braucht ein Coprozessor zum Erledigen seiner Aufgaben auch Daten. Je nach Programmiermodell überträgt die Laufzeitumgebung Daten automatisch oder muss explizit beauftragt werden. OpenMP 4.0 geht den letzteren Weg, während Cilk Plus einen gemeinsamen Speicher zwischen Host und Coprozessor emuliert.

Tabelle 1: Mögliche Klauseln für OpenMP-Offload-Direktiven

Tabelle 1: Mögliche Klauseln für OpenMP-Offload-Direktiven

Tabelle 1 listet alle möglichen Klauseln für die target-Direktive von OpenMP auf. Mittels der map-Klausel können Programmierer/-innen Datenübertragungen auslösen und deren Richtung beeinflussen. Für viele Fälle versucht der Compiler automatisch die richtige Übertragungsart herauszufinden, allerdings gelingt das nicht immer. Nur für einfache Objekte (Variablen, Arrays mit fester Länge) schafft er es selbst; in allen anderen Fällen muss eingegriffen und eine entsprechende Klausel verwendet werden. Als weitere Einschränkung für die Daten gilt, dass diese bitweise kopierbar sein müssen, was C++-Objekte mit virtuellen Methoden und Strukturen/Arrays mit Zeigern ausschließt.

Listing 3


#include <mkl.h>

void mxm_omp(double *A, double *B, double *C, int m, int k, int n) {

/* Offload an Coprocessor 0, mit A und B als Eingabe, C als Ausgabe.

Einschränkung: Offload erfolgt nur, wenn m,k,n > 512 */

#pragma omp target device(0) if(m > 512 && k > 512 && n > 512) \

map(to:A[0:m*k]) map(to:B[0:k*n]) map(from:C[0:m*n])

{

double alpha = 1.0;

double beta = 0.0;

/* Aufruf an MKLs dgemm-Funktion */

cblas_dgemm(CblasRowMajor, CblasNoTrans, CblasNoTrans,

m, n, k, alpha, A, k, B, n, beta, C, n);

}

}

Listing 3 zeigt eine mögliche Anwendung der genannten Klauseln für die Multiplikation zweier Matrizen. Der Einfachheit halber benutzen wir an dieser Stelle die dgemm-Funktion der Intel Math Kernel Library, die bereits passend für alle Intel-Plattformen optimiert ist. Das Beispiel verwendet den ersten Coprozessor im System, aber nur, wenn die Matrixgrößen mindestens 512 in jeder Dimension sind. Die map-Klauseln geben an, dass die Matrizen A und B Eingabedaten sind, die vom Host zum Coprozessor zu übertragen sind. Die Matrix C enthält das Ergebnis und muss daher vom Coprozessor zum Host zurückübertragen werden. Die eckigen Klammern in der map-Klausel sind an die Array-Ausdrücke von Cilk Plus und Fortran angelehnt und gestatten nicht nur die Angabe (Start-)Index, sondern auch ein Längen nach dem Doppelpunkt. Der Code in Listing 1 nutzt das, um dem Laufzeitsystem mitzuteilen, wie viele Datenelemente übertragen werden müssen.

Wer sich mit der allgemeinen dgemm-Operation auskennt, weiß, dass diese auch C als Eingabe verwenden kann und dann die weitergefasste Operation C = alpha·A×B+beta·C durchführt. In Listing 1 haben wir aber beta auf 0 gesetzt, sodass die ursprünglichen Matrixelemente von C nicht relevant sind. Wäre beta ungleich 0, so müsste map(from:C[0:m*n]) durch map(tofrom:C[0:m*n]) oder kurz map(C[m*n]) ersetzt werden.

Listing 4


_Cilk_shared

void mxm_cilk(_Cilk_shared double *A, _Cilk_shared double *B,

_Cilk_shared double *C, int m, int k, int n) {

double alpha = 1.0;

double beta = 0.0;

cblas_dgemm(CblasRowMajor, CblasNoTrans, CblasNoTrans,

m, n, k, alpha, A, k, B, n, beta, C, n);

}

 

int aufrufer() {

/* Allokiere A, B und C im gemeinsamen Speicherbereich.

Alle Zeiger zeigen auf Daten im gemeinsamen Speicherbereich */

_Cilk_shared double *A =

(_Cilk_shared double*) _Offload_shared_malloc(sizeof(double) * M * K);

_Cilk_shared double *B =

(_Cilk_shared double*) _Offload_shared_malloc(sizeof(double) * K * N);

_Cilk_shared double *C =

(_Cilk_shared double*) _Offload_shared_malloc(sizeof(double) * M * N);

 

/* Aufruf der mxm_cilk-Funktion auf dem Coprozessor. Daten warden automatisch zwischen Host und Coprozessor übertragen */

_Cilk_offload mxm_cilk(A, B, C, M, K, N);

 

_Offload_shared_free(A);

_Offload_shared_free(B);

_Offload_shared_free(C);

}

Listing 4 zeigt das dgemm-Bespiel mit Offloading per Cilk Plus. Datentransfers finden automatisch statt, jedoch muss der Programmierer Daten, die zwischen Host und Coprozessor ausgetauscht werden sollen, mit _Cilk_shared markieren, sodass das Laufzeitsystem über diese Daten Bescheid weiß. Das Laufzeitsystem legt diese Daten in einem speziellen Bereich des Adressraums ab, sodass der gemeinsame Speicher emuliert werden kann. Um dynamische Daten in diesem Adressbereich allokieren und freigeben zu können, existieren _Offload_shared_malloc und _Offload_shared_free, die das normale malloc und free ersetzen. In C++ muss hierfür der new-Operator überladen oder Allokatoren (z. B. für STL-Container) implementiert werden, die diese Funktionen entsprechend verwenden. Der nötige Aufwand für diese Änderungen wird dann aber durch vereinfachtes Datenmanagement während des Offloads kompensiert. Außerdem können C++-Objekte und beliebige Zeigerstrukturen übertragen werden, sofern alle Daten im gemeinsamen Speicherbereich abgelegt wurden.

Verlustbehaftete Komprimierung

Die vorherigen Bespiele wurden nicht zufällig gewählt. Matrixoperationen spielen in vielen Anwendungsbereichen im Höchstleistungsrechnen eine tragende Rolle. Sei es Quantenchemie oder die Lösung von partiellen Differentialgleichungen, beinahe überall finden sich derartige Matrixoperationen. An dieser Stelle sei eine etwas andere Verwendung erwähnt: verlustbehaftete Bildkompression.

Abbildung 2 zeigt eine solche Kompression des Bilds links oben. Von links nach rechts erhöht sich der Detailgrad massiv. Die komprimierten Bilder wurden durch eine so genannte Singulärwertzerlegung [1] aus dem Ausgangsbild mittels eines Python-Programms erzeugt. Für die Singulärwertzerlegung wird eine Matrix M so zerlegt, dass drei Matrizen U, ∑, und V entstehen, wobei M=U×∑×VT. gilt. Ist M eine m×n-Matrix mit reellen Werten, dann ist U eine unitäre m×mMatrix, ∑ eine reell-wertige m×n-Diagonalmatrix und VT die transponierte einer unitären n×n-Matrix. Bei der Bildkompression wird das Bild als Matrix M von Farbwerten angesehen und entsprechend zerlegt. Betrachtet man nur einen Teil der Matrix ∑ (alle stark von 0 verschiedenen Einträge oder eine Teilmatrix) für die Rekonstruktion, so wird das Bild effektiv komprimiert.

Abb. 2: Verlustbehaftete Komprimierung mittels Singulärwertzerlegung

Abb. 2: Verlustbehaftete Komprimierung mittels Singulärwertzerlegung

Listing 5


import sys

import numpy as np

from PIL import Image

 

def read_image(file):

# Lade Bilddatei und erzeuge Graustufen

img = Image.open(file)

img = img.convert("LA")

return img

 

def write_image(image, file):

# Konvertiere nach RGB und speichere Bild

image = image.convert("RGB")

image.save(file)

 

def compute_svd(image):

# Konvertiere das Bild in Matrix

mtx = np.asarray(image.getdata(band=0), float)

mtx.shape = (image.size[1], image.size[0])

mtx = np.matrix(mtx)

# Berechne Singulärwertzerlegung, liefert Tupel

return np.linalg.svd(mtx)

 

def reconstruct_image(U, sigma, V):

# Berechne M = U x sigma x V

reconstructed = U * sigma * V

# Erzeuge Bild aus Matrixrepräsentation

image = Image.fromarray(reconstructed)

return image

 

# Aufruf des Programs mit:

#   python svd.py eingabe.jpg #anzahl_singulärwerte ausgabe.jpg

filename = sys.argv[1]

num_values = int(sys.argv[2])

output = sys.argv[3]

 

# Lade Bild und berechne Singulärwertzerlegung

image = read_image(filename)

U, sigma, V = compute_svd(image)

 

# Komprimiere unter Benutzung von num_values Singulärwerten

compressed_U = U[:, :num_values]

compressed_sigma = sigma[:num_values]

compressed_V = V[:num_values, :]

 

# Rekonstruiere das komprimierte Bild

reconstructed = reconstruct_image(compressed_U, compressed_sigma, compressed_V)

write_image(reconstructed, output)

Listing 5 zeigt den Python-Code zur Singulärwertzerlegung eines Grauwertbilds. Auf jegliche Fehlerbehandlung wurde aus Platzgründen verzichtet. Das Beispiel nutzt hierfür unter anderem das PIL-Paket und dessen Image-Klasse. Die Funktion read_image liest ein Bild ein und konvertiert es in Graustufen. Damit lassen sich die üblichen Grafikformate leicht lesen, verarbeiten und abspeichern. Mittels der Funktion write_image wird das Graustufenbild zunächst in den RGB-Farbraum konvertiert und anschließend abgespeichert.

Die Funktion compute_svd errechnet die Singulärwertzerlegung des Graustufenbilds. Das Numpy-Paket stellt eine bereits fertige Implementierung des Algorithmus zur Zerlegung des Bilds bereit. Dieser findet sich im Modul numpy.linalg. Nach erfolgter Zerlegung gibt die svd-Funktion die Matrizen U, ∑ und V als Tupel zurück und compute_svd reicht sie an den Aufrufer weiter.

Die Rekonstruktion des Bilds erfolgt durch Umkehr der oben skizzierten Zerlegung. Es sind daher zwei Matrixmultiplikationen nötig, um aus U, ∑ und V wieder die Bildmatrix zu erzeugen. Die Funktion reconstruct_image nutzt hierfür die Klasse numpy.matrix, welche den Multiplikationsoperator für Matrix-Matrix-Operationen überlädt. Anschließend erzeugt die Funktion wieder ein Image-Objekt und initialisiert die Bildpunkte mit der errechneten neuen Matrix. Die eigentliche Kompression findet vor dem Aufruf von reconstruct_image durch Reduktion der Zahl der betrachteten Singulärwerte in der Matrix ∑ statt.

Schlangenbeschwörung

Das Python-Modul pyMIC bietet zur Integration des Intel Xeon Phi Coprozessors in Python-Programme, die Numpy zur Speicherung von Arrays verwenden, eine leichtgewichtige Offload-Schnittstelle. pyMIC stellt für diesen Zweck passende Pufferobjekte bereit, die den Transfer von Array-Daten zwischen Host und Coprozessor und zurück erlauben. Außerdem bietet pyMIC Möglichkeiten, native Funktionen (C/C++ oder Fortran) aufzurufen und an diese die Puffer zu übergeben. Obwohl hiermit (noch) kein Python-Code als Offload-Region verwendet werden kann, so erlaubt das Modul dennoch die Nutzung des Coprozessors zur Beschleunigung von Python-Applikationen.

Listing 6


import pyMIC as mic

 

# Handle für Coprozessor 0

device = mic.devices[0]

 

# Lade dynamische Bibliothek mit Offload-Code

device.load_library("libsvd.so")

 

def reconstruct_image_offloaded(U, sigma, V):

# Erzeuge leere Puffer auf dem Coprozessor

offl_tmp   = device.empty((U.shape[0], U.shape[1]),

dtype=float, update_host=False)

offl_res   = device.empty((U.shape[0], V.shape[1]),

dtype=float, update_host=False)

 

# Binde Arrays auf dem Host an Puffer

offl_U     = device.bind(U)

offl_sigma = device.bind(sigma)

offl_V     = device.bind(V)

 

# Aufruf der dgemm-Funktion auf dem Coprozessor

alpha = 1.0

beta  = 0.0

m, k, n = U.shape[0], U.shape[1], sigma.shape[1]

device.invoke_kernel("dgemm_kernel", offl_U, offl_sigma, offl_tmp,

m, n, k, alpha, beta)

m, k, n = offl_tmp.shape[0], offl_tmp.shape[1], V.shape[1]

device.invoke_kernel("dgemm_kernel", offl_tmp, offl_V, offl_res,

m, n, k, alpha, beta)

 

# Erzeuge Bild aus Matrixrepräsentation

image = Image.fromarray(offl_res.update_host().array)

return image

Die oben verwendete Funktion reconstruct_image lässt sich mit pyMIC wie in Listing 6 dargestellt auf dem Coprozessor ausführen. Im ersten Schritt wird ein Handle für das Zielgerät geholt, über das Objektpuffer erzeugt und Funktionen aufgerufen werden können.

Die Funktion reconstruct_image_offloaded erzeugt zunächst eine Menge an Pufferobjekten. Da dgemm zweimal aufgerufen werden muss, erzeugt man zunächst ein temporäres Array (offl_tmp), das das Zwischenergebnis der ersten Matrixmultiplikation speichert. Hinzu kommt ein Puffer (offl_res) für das Ergebnis. Die Numpy Arrays zur Speicherung der drei Matrizen werden an Pufferobjekte gebunden. Somit lassen sich die Arrays leicht aktualisieren. Die Funktion update_host() überträgt geänderte Daten vom Coprozessor in das gebundene Array auf dem Host. Die Gegenrichtung übernimmt die Funktion update_device().

Als Nächstes werden die Parameter für dgemm initialisiert, sodass invoke_kernel() die Funktion dgemm_kernel (Listing 7) auf dem Coprozessor aufrufen kann. Diese packt zunächst die Funktionsargumente durch Casting und Dereferenzierung auf den richtigen Datentyp aus. Danach ruft sie wieder die dgemm-Funktion aus der Intel Math Kernel Library auf. Zwischen den Aufrufen von dgemm_kernel finden keine Datentransfers zwischen Host und Coprozessor statt. Bei Erzeugung von Puffern kann mittels des Arguments update_host bzw. update_device angegeben werden, ob ein initialer Datentransfer stattfinden soll.

Listing 7


#include <pymic_kernel.h>

#include <mkl.h>

 

/* Definiere dgemm_kernel als Offload-Funktion für Python */

PYMIC_KERNEL

void dgemm_kernel(int argc, uintptr_t argptr[], size_t sizes[]) {

/* Packe Funktionsargumente aus */

double *A = (double*) argptr[0];

double *B = (double*) argptr[1];

double *C = (double*) argptr[2];

int m = *(long int*) argptr[3];

int n = *(long int*) argptr[4];

int k = *(long int*) argptr[5];

double alpha = *(double*) argptr[6];

double beta = *(double*) argptr[7];

 

/* Rufe MKLs dgemm-Funktion auf */

cblas_dgemm(CblasRowMajor, CblasNoTrans, CblasNoTrans,

m, n, k, alpha, A, k, B, n, beta, C, n);

}

Fazit

Offload-Programmierung muss nicht schwer und aufwändig sein. Mit den richtigen Analysen finden sich mögliche Offload-Regionen. OpenMP 4.0 gestattet Programmierern einen inkrementellen Ansatz zur schrittweisen Implementierung von Offload-Regionen und deren Verfeinerung bzw. Optimierung. Intel Cilk Plus erweitert die Syntax von C/C++ um Offload-Schlüsselworte und unterstützt ebenfalls eine inkrementelle Herangehensweise. Alle nötigen Werkzeuge sind Teil des Intel Parallel Studio XE 2015 für Linux und Windows. Trotz Einschränkungen ist es außerdem möglich, mittels pyMIC den Intel Xeon Phi Coprocessor ohne großen Aufwand anzusprechen.

Links & Literatur

[1] Bronstein, Semendjajev; Musiol, Mühlig: „Taschenbuch der Mathematik“, 8. Auflage, Verlag Harri Deutsch

Aufmacherbild: integrated microchip Foto via Shutterstock / Urheberrecht: Robert Lucian Crusitu

Unsere Redaktion empfiehlt:

Relevante Beiträge

Meinungen zu diesem Beitrag

X
- Gib Deinen Standort ein -
- or -