4

我正在尝试使用 Alea 来加速我正在开发的程序,但我需要一些帮助。

我需要做的是对存储在两个数组中的值进行大量位计数和按位运算。

对于我的第一个数组的每个元素,我必须对我的第二个数组的每个元素进行按位 & 运算,然后计算 & 结果中设置为 1 的位。

如果结果大于/等于某个值,我需要退出内部 for 并转到我的第一个数组的下一个元素。

第一个数组通常很大,有数百万个元素,第二个数组通常少于 200.000 个元素。

尝试并行执行所有这些操作,这是我的代码:

[GpuManaged]
private long[] Check(long[] arr1, long[] arr2, int limit)
{
    Gpu.FreeAllImplicitMemory(true);
    var gpu = Gpu.Default;
    long[] result = new long[arr1.Length];
    gpu.For(0, arr1.Length, i =>
    {
        bool found = false;
        long b = arr1[i];
        for (int i2 = 0; i2 < arr2.Length; i2++)
        {
            if (LibDevice.__nv_popcll(b & arr2[i2]) >= limit)
            {
                found = true;
                break;
            }
        }
        if (!found)
        {
            result[i] = b;
        }
    });
    return result;
}

这按预期工作,但比我在四核 CPU 上并行运行的版本快一点。

我当然在这里遗漏了一些东西,这是我第一次尝试编写 GPU 代码。

顺便说一句,我的 NVIDIA 是 GeForce GT 740M。

编辑

下面的代码比前面的代码快 2 倍,至少在我的 PC 上是这样。非常感谢 Michael Randall 为我指明了正确的方向。

private static int[] CheckWithKernel(Gpu gpu, int[] arr1, int[] arr2, int limit)
{
    var lp = new LaunchParam(16, 256);
    var result = new int[arr1.Length];
    try
    {
        using (var dArr1 = gpu.AllocateDevice(arr1))
        using (var dArr2 = gpu.AllocateDevice(arr2))
        using (var dResult = gpu.AllocateDevice<int>(arr1.Length))
        {
            gpu.Launch(Kernel, lp, arr1.Length, arr2.Length, dArr1.Ptr, dArr2.Ptr, dResult.Ptr, limit);
            Gpu.Copy(dResult, result);
            return result;
        }
    }
    finally
    {
        Gpu.Free(arr1);
        Gpu.Free(arr2);
        Gpu.Free(result);
    }
}

private static void Kernel(int a1, int a2, deviceptr<int> arr1, deviceptr<int> arr2, deviceptr<int> arr3, int limit)
{
    var iinit = blockIdx.x * blockDim.x + threadIdx.x;
    var istep = gridDim.x * blockDim.x;
    for (var i = iinit; i < a1; i += istep)
    {
        bool found = false;
        int b = arr1[i];
        for (var j = 0; j < a2; j++)
        {
            if (LibDevice.__nv_popcll(b & arr2[j]) >= limit)
            {
                found = true;
                break;
            }
        }
        if (!found)
        {
            arr3[i] = b;
        }
    }
}
4

1 回答 1

2

更新

似乎固定不适用于 GCHandle.Alloc()

然而,这个答案的重点是,您将从直接内存访问中获得更大的性能提升。

http://www.aleagpu.com/release/3_0_3/doc/advanced_features_csharp.html

直接使用设备内存

设备内存提供了更大的灵活性,因为它还允许所有类型的指针算术。设备内存分配有

Memory<T> Gpu.AllocateDevice<T>(int length)
Memory<T> Gpu.AllocateDevice<T>(T[] array)

第一个重载在选定的 GPU 上创建指定类型T和长度的设备内存对象。第二个在 GPU 上分配存储并将 .NET 数组复制到其中。两者都返回一个 Memory<T>对象,该对象实现IDisposable并因此可以支持 using 语法,以确保在 Memory<T>对象超出范围后正确处理。A Memory<T>对象具有确定长度、GPU 或它所在的设备的属性。该 Memory<T>.Ptr属性返回 a deviceptr<T>,可在 GPU 代码中用于访问实际数据或执行指针运算。以下示例说明了设备指针的简单用例。内核仅对由偏移量定义的部分数据进行操作。

using (var dArg1 = gpu.AllocateDevice(arg1))
using (var dArg2 = gpu.AllocateDevice(arg2))
using (var dOutput = gpu.AllocateDevice<int>(Length/2))
{           
    // pointer arithmetics to access subset of data
    gpu.Launch(Kernel, lp, dOutput.Length, dOutput.Ptr, dArg1.Ptr + Length/2, dArg2.Ptr + Length / 2);

    var result = dOutput.ToArray();

    var expected = arg1.Skip(Length/2).Zip(arg2.Skip(Length/2), (x, y) => x + y);

    Assert.That(result, Is.EqualTo(expected));
}

原始答案

忽略正在发生的逻辑,或者这与 GPU 代码的相关性。但是,您可以通过使用标志将数组固定在内存中并使用直接指针访问(如果您可以运行代码)来补充您的Parallel例程并可能加快速度GCHandle.Alloc()GCHandleType.Pinnedunsafe

笔记

  • 您会因固定内存而受到打击,但是对于大型阵列,您可以通过直接访问实现很多性能*

  • 您必须在构建属性中标记您的程序集不安全*

  • 这显然是未经测试的,只是一个例子*

  • 你可以使用固定的,但是 Parallel Lambda 让它变得更复杂

例子

private unsafe long[] Check(long[] arr1, long[] arr2, int limit)
{   
   Gpu.FreeAllImplicitMemory(true);
   var gpu = Gpu.Default;    
   var result = new long[arr1.Length];

   // Create some pinned memory
   var resultHandle = GCHandle.Alloc(result, GCHandleType.Pinned);
   var arr2Handle = GCHandle.Alloc(result, GCHandleType.Pinned);
   var arr1Handle = GCHandle.Alloc(result, GCHandleType.Pinned);

   // Get the addresses
   var resultPtr = (int*)resultHandle.AddrOfPinnedObject().ToPointer();
   var arr2Ptr = (int*)arr2Handle.AddrOfPinnedObject().ToPointer();
   var arr1Ptr = (int*)arr2Handle.AddrOfPinnedObject().ToPointer();

   // I hate nasty lambda statements. I always find local methods easier to read.    
   void Workload(int i)
   {
      var found = false;    
      var b = *(arr1Ptr + i);

      for (var j = 0; j < arr2.Length; j++)
      {
         if (LibDevice.__nv_popcll(b & *(arr2Ptr + j)) >= limit)
         {
            found = true;
            break;
         }
      }

      if (!found)
      {
         *(resultPtr + i) = b;
      }
   }

   try
   {
      gpu.For(0, arr1.Length, i => Workload(i));
   }
   finally 
   {
      // Make sure we free resources
      arr1Handle.Free();
      arr2Handle.Free();
      resultHandle.Free();
   } 
   return result;    
}

其他资源

GCHandle.Alloc 方法(对象)

一个新的 GCHandle 保护对象免受垃圾收集。当不再需要此 GCHandle 时,必须将其与 Free 一起释放。

GCHandleType 枚举

Pinned :这种句柄类型类似于Normal,但允许获取被固定对象的地址。这会阻止垃圾收集器移动对象,从而降低垃圾收集器的效率。使用 Free 方法尽快释放分配的句柄。

不安全的代码和指针(C# 编程指南)

在公共语言运行时 (CLR) 中,不安全的代码被称为无法验证的代码。C# 中的不安全代码不一定是危险的;它只是 CLR 无法验证其安全性的代码。因此,如果 CLR 位于完全受信任的程序集中,则 CLR 只会执行不安全的代码。如果您使用不安全的代码,您有责任确保您的代码不会引入安全风险或指针错误。

  • 注意,此后有更新,这是:

http://www.aleagpu.com/release/3_0_3/doc/advanced_features_csharp.html

现在是这样的:

http://www.aleagpu.com/release/3_0_4/doc/advanced_features_csharp.html

一些示例和信息在 3.0.4 版中已更改或移动。

于 2018-02-28T11:37:03.657 回答