Hybridizer是來自Altimesh的編譯器,可以讓人們采用C#代碼或.NET程序集編程GPU和其他加速器。Hybridizer使用修飾符號來表達(dá)并行性,可以生成針對多核CPU和GPU優(yōu)化的源代碼或二進(jìn)制文件。在這篇博文中演示了CUDA的目標(biāo)。
圖1 Hybridizer編譯管線
圖1顯示了Hybridizer編譯管線。使用Parallel.For之類的并行化模式,或者像在CUDA中一樣明確地分配并行工作,可以從加速器的計算能力中受益,而無需了解其內(nèi)部架構(gòu)的所有細(xì)節(jié)。下面是一個使用Parallel.For和lambda的簡單示例。
人們可以使用NVIDIA Nsight Visual Studio Edition在GPU上調(diào)試和分析這些代碼。 Hybridizer實現(xiàn)了先進(jìn)的C#功能,其中包括虛函數(shù)和泛型。
在哪里可以獲得Hybridizer
Hybridizer有兩個版本:
Hybridizer Software Suite:支持CUDA,AVX,AVX2,AVX512目標(biāo)和輸出源代碼。這個源代碼需要被審查,這在某些企業(yè)(如投資銀行)是強制性的。Hybridizer軟件套件根據(jù)客戶要求獲得許可。
Hybridizer Essentials:僅啟用CUDA目標(biāo)并僅輸出二進(jìn)制文件。 Hybridizer Essentials是一個免費的Visual Studio擴展程序,沒有硬件限制。人們可以在GitHub上找到一組基本代碼示例和教育資料。這些樣本也可以用來重現(xiàn)其性能結(jié)果。
調(diào)試和分析
使用調(diào)試信息進(jìn)行編譯時,可以在Microsoft Visual Studio中調(diào)試Hybridizer C#/ .NET代碼,同時在目標(biāo)硬件上運行優(yōu)化代碼。例如,用C#編寫的程序可以在Visual Studio中的C#文件中創(chuàng)建一個斷點,可以瀏覽駐留在GPU上的本地變量和對象數(shù)據(jù)。
圖2 使用Hybridizer和NVIDIA Nsight Visual Studio Edition調(diào)試GPU上運行的C#代碼
人們可以在復(fù)雜項目中集成Hybridizer,即使在代碼不可用或模糊的庫中,這是因為Hybridizer在MSIL字節(jié)碼上運行。在博客文章中展示了使用Hybridizer加速AForge圖像處理庫而沒有修改庫的能力。在MSIL字節(jié)碼上運行也支持在.Net虛擬機之上構(gòu)建的各種語言,比如VB.Net和F#。
所有這些靈活性不會以犧牲性能損失為代價。正如基準(zhǔn)測試所示,Hybridizer產(chǎn)生的代碼可以像手寫代碼一樣執(zhí)行。人們可以使用性能分析器(例如NVIDIA Nsight和NVIDIA Visual Profiler)來測量生成的二進(jìn)制文件的性能,其性能指標(biāo)指的是原始源代碼(例如C#)。
舉一個簡單的例子:Mandelbrot
作為第一個例子,演示了在NVIDIA GeForce GTX 1080 Ti GPU(Pascal架構(gòu),計算能力6.1)上運行的Mandelbrot分形的渲染。
Mandelbrot C#代碼
以下代碼片斷顯示了plain C#。它在CPU上平穩(wěn)運行,沒有任何性能損失,因為大多數(shù)代碼都是屬性的修改,在運行時沒有任何影響(例如Run方法中的EntryPoint屬性)。
[EntryPoint]
public static void Run(float[,] result)
{
int size = result.GetLength(0);
Parallel2D.For(0, size, 0, size, (i, j) => {
float x = fromX + i * h;
float y = fromY + j * h;
result[i, j] = IterCount(x, y);
});
}
public static float IterCount(float cx, float cy)
{
float result = 0.0F;
float x = 0.0f, y = 0.0f, xx = 0.0f, yy = 0.0f;
while (xx + yy <= 4.0f && result < maxiter) {
xx = x * x;
yy = y * y;
float xtmp = xx - yy + cx;
y = 2.0f * x * y + cy;
x = xtmp;
result++;
}
return result;
}
EntryPoint屬性告訴Hybridizer生成一個CUDA內(nèi)核。多維數(shù)組映射到內(nèi)部類型,而Parallel2D.For映射到2D執(zhí)行網(wǎng)格。給定幾行Boilerplate(樣板)代碼,可以透明地在GPU上運行這些代碼。
float[,] result = new float[N,N];
HybRunner runner = HybRunner.Cuda("Mandelbrot_CUDA.dll").SetDistrib(32, 32, 16, 16, 1, 0);
dynamic wrapper = runner.Wrap(new Program());
wrapper.Run(result);
剖析
我們使用Nvidia Nsight Visual Studio Edition分析器來分析此代碼。將C#代碼鏈接到CUDA源代碼視圖中的PTX,如圖3所示。
圖3在CUDA源代碼視圖中分析Mandelbrot C#代碼
分析器允許與CUDA C ++代碼相同的調(diào)查級別。
就性能而言,這個例子達(dá)到峰值計算FLOP / s的72.5%。這是CUDA C ++人工編寫的相同代碼的83%。
圖4 Profiler輸出顯示了GPU上Mandelbrot代碼的GPU利用率和執(zhí)行效率。它實現(xiàn)的效率幾乎與人工編寫CUDA C ++代碼一樣高效。 使用Hybridizer提供的擴展控件,可以從C#代碼中獲得更好的性能。如下面的代碼所示,其語法與CUDA C ++非常相似。[EntryPoint]
public static void Run(float[] result)
{
for (int i = threadIdx.y + blockIdx.y * blockDim.y; i < N; i += blockDim.y * gridDim.y)
{
for (int j = threadIdx.x + blockIdx.x * blockDim.x; j < N; j += blockDim.x * gridDim.x)
{
float x = fromX + i * h;
float y = fromY + j * h;
result[i * N + j] = IterCount(x, y);
}
}
}
在這個案例中,生成的代碼和人工編寫的CUDA C ++代碼的性能完全相同,達(dá)到峰值FLOP/s的87%,如圖5所示。
圖5分析人工優(yōu)化的Mandelbrot C#代碼
泛型和虛函數(shù)
Hybridizer支持設(shè)備功能中的泛型和虛函數(shù)調(diào)用。現(xiàn)代編程語言的這些基本概念有助于代碼模塊化并提高表達(dá)能力。但是,C#中的類型解析是在運行時完成的,這會導(dǎo)致一些性能損失。.NET的泛型可以在保持靈活性的同時實現(xiàn)更高的性能:Hybridizer將泛型映射到C++模板,C ++模板在編譯時解析,允許函數(shù)內(nèi)聯(lián)和過程間優(yōu)化。另一方面,虛函數(shù)調(diào)用被映射到其中實例方法被注冊的虛函數(shù)表。
通過兩個屬性HybridTemplateConcept和HybridRegisterTemplate(在設(shè)備代碼中觸發(fā)實際的模板實例化)給模板實例化提示。作為一個例子,我們來看看兩個版本中的一個簡單的stream benchmark,一個使用虛函數(shù)調(diào)用,另一個使用模板映射。該基準(zhǔn)依賴于一個通用的接口IMyArray暴露出下標(biāo)運算符:
[HybridTemplateConcept]
public interface IMyArray {
double this[int index] { get; set; }
}
這些操作符必須與設(shè)備功能"Hybridized(雜交)"。為此,我們把Kernel屬性放在實現(xiàn)類中。
public class MyArray : IMyArray {
double[] _data;
public MyArray(double[] data) {
_data = data;
}
[Kernel]
public double this[int index] {
get { return _data[index]; }
set { _data[index] = value; }
}
}
虛擬功能調(diào)用
在第一個版本中,使用接口編寫了一個流算法,沒有進(jìn)一步提示編譯器。public class MyAlgorithmDispatch {
IMyArray a, b;
public MyAlgorithmDispatch(IMyArray a, IMyArray b) {
this.a = a;
this.b = b;
}
[Kernel]
public void Add(int n) {
IMyArray a = this.a;
IMyArray b = this.b;
for (int k = threadIdx.x + blockDim.x * blockIdx.x;
k < n;
k += blockDim.x * gridDim.x) {
a[k] += b[k];
}
}
}
因為把a和b上的下標(biāo)運算符稱為接口,所以在MSIL中有一個callvirt。IL_002a: ldloc.3
IL_002b: ldloc.s 4
IL_002d: callvirt instance float64 Mandelbrot.IMyArray::get_Item(int32)
IL_0032: ldloc.1
IL_0033: ldloc.2
IL_0034: callvirt instance float64 Mandelbrot.IMyArray::get_Item(int32)
IL_0039: add
IL_003a: callvirt instance void Mandelbrot.IMyArray::set_Item(int32, float64檢查生成的二進(jìn)制表明Hybridizer在虛函數(shù)表中生成了一個查找,如圖6所示。
圖6. PTX中的虛函數(shù)調(diào)用 這個版本的算法消耗32個寄存器,并獲得271GB/s的帶寬,如圖7所示。在同一硬件上,CUDA Toolkit中的帶寬測試示例達(dá)到352GB/s。
圖7由于虛函數(shù)調(diào)用而實現(xiàn)的低帶寬
虛函數(shù)表會導(dǎo)致更多的注冊壓力,并防止內(nèi)聯(lián)。
通用要求
采用泛型寫了第二個版本,要求Hybridizer生成模板代碼。
[HybridRegisterTemplate(Specialize = typeof(MyAlgorithm))]
public class MyAlgorithm where T : IMyArray
{
T a, b;
[Kernel]
public void Add(int n)
{
T a = this.a;
T b = this.b;
for (int k = threadIdx.x + blockDim.x * blockIdx.x;
k < n;
k += blockDim.x * gridDim.x)
a[k] += b[k];
}
}
public MyAlgorithm(T a, T b)
{
this.a = a;
this.b = b;
}
}
使用RegisterTemplate屬性,Hybridizer將生成相應(yīng)的模板實例。然后生成內(nèi)聯(lián)函數(shù)的調(diào)用,如圖8所示。
圖8使用泛型參數(shù)生成內(nèi)聯(lián)函數(shù)調(diào)用,而不是虛函數(shù)表查找
其通用參數(shù)性能要好得多,達(dá)到339GB/s,性能提高25%(如圖9所示),帶寬測試為96%。
圖9 由于函數(shù)內(nèi)聯(lián),泛型實現(xiàn)了更高的帶寬開始使用Hybridizer
Hybridizer支持各種C#特性,允許代碼分解和表達(dá)。Visual Studio和Nsight(調(diào)試器和分析器)中的集成為人們提供了一個安全高效的開發(fā)環(huán)境。即使在非常復(fù)雜的高度定制的代碼上,Hybridizer也可以實現(xiàn)出色的GPU性能。
人們可以從Visual Studio Marketplace下載Hybridizer Essentials。查看在github上的SDK。