windows C++ 并行编程-矩阵乘法

下面我们尝试分步演练演示如何使用 C++ AMP 加速矩阵乘法的执行。 提供了两种算法,一种不使用平铺,一种使用平铺,看看两者的差别。

在 Visual Studio 中创建项目
  • 在菜单栏上,选择“文件”>“新建”>“项目”,打开“创建新项目”对话框 。
  • 在对话框顶部,将“语言”设置为“C++”,将“平台”设置为“Windows”,并将“项目类型”设置为“控制台”。
  • 从筛选的项目类型列表中,选择“空项目”,然后选择“下一步”。 在下一页中的“名称”框内输入“MatrixMultiply”以指定项目的名称,并根据需要指定项目位置。
  • 选择“创建”按钮创建客户端项目。
  • 在“解决方案资源管理器”中,打开“源文件”的快捷菜单,然后选择“添加”>“新项”。
  • 在“添加新项”对话框中,选择“C++ 文件(.cpp)”,在“名称”框中输入“MatrixMultiply.cpp”,然后选择“添加”按钮。
不使用平铺的乘法

在本部分中,请考虑两个矩阵(A 和 B)的乘法,定义如下:

A 是 3x2 矩阵,B 是 2x3 矩阵。 A 和 B 的乘积是以下 3x3 矩阵。 乘积通过逐个元素将 A 的行乘以 B 的列进行计算。

 

在不使用 C++ AMP 的情况下相乘

打开 MatrixMultiply.cpp,并使用以下代码替换现有代码。

#include <iostream>

void MultiplyWithOutAMP() {
    int aMatrix[3][2] = {{1, 4}, {2, 5}, {3, 6}};
    int bMatrix[2][3] = {{7, 8, 9}, {10, 11, 12}};
    int product[3][3] = {{0, 0, 0}, {0, 0, 0}, {0, 0, 0}};

    for (int row = 0; row < 3; row++) {
        for (int col = 0; col < 3; col++) {
            // Multiply the row of A by the column of B to get the row, column of product.
            for (int inner = 0; inner < 2; inner++) {
                product[row][col] += aMatrix[row][inner] * bMatrix[inner][col];
            }
            std::cout << product[row][col] << "  ";
        }
        std::cout << "\n";
    }
}

int main() {
    MultiplyWithOutAMP();
    getchar();
}

 该算法是矩阵乘法定义的简单实现。 它不使用任何并行或线程算法来减少计算时间。

使用 C++ AMP 相乘

1. 在 MatrixMultiply.cpp 中,在 main 方法之前添加以下代码。

void MultiplyWithAMP() {
int aMatrix[] = { 1, 4, 2, 5, 3, 6 };
int bMatrix[] = { 7, 8, 9, 10, 11, 12 };
int productMatrix[] = { 0, 0, 0, 0, 0, 0, 0, 0, 0 };

array_view<int, 2> a(3, 2, aMatrix);

array_view<int, 2> b(2, 3, bMatrix);

array_view<int, 2> product(3, 3, productMatrix);

parallel_for_each(product.extent,
   [=] (index<2> idx) restrict(amp) {
       int row = idx[0];
       int col = idx[1];
       for (int inner = 0; inner <2; inner++) {
           product[idx] += a(row, inner)* b(inner, col);
       }
   });

product.synchronize();

for (int row = 0; row <3; row++) {
   for (int col = 0; col <3; col++) {
       //std::cout << productMatrix[row*3 + col] << "  ";
       std::cout << product(row, col) << "  ";
   }
   std::cout << "\n";
  }
}

 AMP 代码类似于非 AMP 代码。 对 parallel_for_each 的调用为 product.extent 中的每个元素启动一个线程,并替换行和列的 for 循环。 idx 中提供了行和列的单元格的值。 可以使用 [] 运算符和索引变量,或者 () 运算符和行列变量来访问 array_view 对象的元素。 示例演示两种方法。 array_view::synchronize 方法将 product 变量的值复制回 productMatrix 变量。

2. 在 MatrixMultiply.cpp 的顶部添加以下 include 和 using 语句。 

#include <amp.h>
using namespace concurrency;

3. 修改 main 方法以调用 MultiplyWithAMP 方法。

int main() {
    MultiplyWithOutAMP();
    MultiplyWithAMP();
    getchar();
}
使用平铺 

平铺是一种将数据分区成大小相等的子集(称为平铺)的技术。 使用平铺时会有三个改变。

  • 可以创建 tile_static 变量。 访问 tile_static 空间中的数据可能比访问全局空间中的数据要快得多。 为每个平铺创建 tile_static 变量的实例,平铺中的所有线程可以访问该变量。 平铺的主要好处是可以从 tile_static 访问中获得性能增益;
  • 可以调用 tile_barrier::wait 方法,以在指定代码行的一个平铺中停止所有线程。 不能保证线程的运行顺序,只有一个平铺中的所有线程都在调用 tile_barrier::wait 时停止,才能继续执行;
  • 可以访问相对于整个 array_view 对象的索引以及相对于平铺的索引。 使用局部索引可使代码更易于阅读和调试;

若要利用矩阵乘法中的平铺,算法必须将矩阵分区成平铺,然后将平铺数据复制到 tile_static 变量中以加快访问速度。 在此示例中,矩阵分区成大小相等的子矩阵。 乘积通过将子矩阵相乘而得出。 此示例中的两个矩阵及其乘积为:

矩阵分区成四个 2x2 矩阵,定义如下:

 

现在可以按以下方式撰写和计算 A 和 B 的乘积:

 

由于矩阵 a 到 h 是 2x2 矩阵,因此其所有乘积及和也是 2x2 矩阵。 它还遵循 A 和 B 的乘积是 4x4 矩阵,与预期一样。 若要快速检查算法,请计算乘积中第一行与第一列相交处的元素的值。 在此示例中,这将是 ae + bg 的第一行与第一列相交处的元素的值。 只需计算每个术语的 ae 和 bg 的第一列与第一行。 ae 的值为 (1 * 1) + (2 * 5) = 11。 (3 * 1) + (4 * 5) = 23 的值为 bg。 最终值是正确的 11 + 23 = 34。

若要实现此算法,代码:

  • 在 parallel_for_each 调用中使用 tiled_extent 对象,而不是 extent 对象;
  • 在 parallel_for_each 调用中使用 tiled_index 对象,而不是 index 对象;
  • 创建 tile_static 变量以保留子矩阵;
  • 使用 tile_barrier::wait 方法停止线程以计算子矩阵的乘积;
使用 AMP 和平铺相乘

1. 在 MatrixMultiply.cpp 中,在 main 方法之前添加以下代码。

void MultiplyWithTiling() {
    // The tile size is 2.
    static const int TS = 2;

    // The raw data.
    int aMatrix[] = { 1, 2, 3, 4, 5, 6, 7, 8, 1, 2, 3, 4, 5, 6, 7, 8 };
    int bMatrix[] = { 1, 2, 3, 4, 5, 6, 7, 8, 1, 2, 3, 4, 5, 6, 7, 8 };
    int productMatrix[] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };

    // Create the array_view objects.
    array_view<int, 2> a(4, 4, aMatrix);
    array_view<int, 2> b(4, 4, bMatrix);
    array_view<int, 2> product(4, 4, productMatrix);

    // Call parallel_for_each by using 2x2 tiles.
    parallel_for_each(product.extent.tile<TS, TS>(),
        [=] (tiled_index<TS, TS> t_idx) restrict(amp)
        {
            // Get the location of the thread relative to the tile (row, col)
            // and the entire array_view (rowGlobal, colGlobal).
            int row = t_idx.local[0];
            int col = t_idx.local[1];
            int rowGlobal = t_idx.global[0];
            int colGlobal = t_idx.global[1];
            int sum = 0;

            // Given a 4x4 matrix and a 2x2 tile size, this loop executes twice for each thread.
            // For the first tile and the first loop, it copies a into locA and e into locB.
            // For the first tile and the second loop, it copies b into locA and g into locB.
            for (int i = 0; i < 4; i += TS) {
                tile_static int locA[TS][TS];
                tile_static int locB[TS][TS];
                locA[row][col] = a(rowGlobal, col + i);
                locB[row][col] = b(row + i, colGlobal);
                // The threads in the tile all wait here until locA and locB are filled.
                t_idx.barrier.wait();

                // Return the product for the thread. The sum is retained across
                // both iterations of the loop, in effect adding the two products
                // together, for example, a*e.
                for (int k = 0; k < TS; k++) {
                    sum += locA[row][k] * locB[k][col];
                }

                // All threads must wait until the sums are calculated. If any threads
                // moved ahead, the values in locA and locB would change.
                t_idx.barrier.wait();
                // Now go on to the next iteration of the loop.
            }

            // After both iterations of the loop, copy the sum to the product variable by using the global location.
            product[t_idx.global] = sum;
        });

    // Copy the contents of product back to the productMatrix variable.
    product.synchronize();

    for (int row = 0; row <4; row++) {
        for (int col = 0; col <4; col++) {
            // The results are available from both the product and productMatrix variables.
            //std::cout << productMatrix[row*3 + col] << "  ";
            std::cout << product(row, col) << "  ";
        }
        std::cout << "\n";
    }
}

此示例明显不同于不使用平铺的示例。 代码使用以下概念步骤:

  • 将 a 的平铺[0,0] 的元素复制到 locA 中。 将 b 的平铺[0,0] 的元素复制到 locB 中。 请注意,product 是平铺的,而不是 a 和 b。 因此,使用全局索引访问 a, b 和 product。 调用 tile_barrier::wait 至关重要。 它会停止平铺中的所有线程,直到填充完 locA 和 locB;
  • 将 locA 与 locB 相乘并将结果放入 product 中;
  • 将 a 的 tile[0,1] 的元素复制到 locA 中。 将 b 的 tile[1,0] 的元素复制到 locB 中;
  • 将 locA 与 locB 相乘并将它们添加到已在 product 中的结果;
  • tile[0,0] 的乘法已完成;
  • 对其他四个平铺重复上述步骤。 没有专门为平铺编制索引,线程可以按任意顺序执行。 执行每个线程时,会相应地为每个平铺创建 tile_static 变量,并且对 tile_barrier::wait 的调用控制程序流;
  • 仔细检查算法时,请注意,每个子矩阵都加载到 tile_static 内存中两次。 数据传输确实需要一段时间。 但是,一旦数据位于 tile_static 内存中,对数据的访问速度要快得多。 由于计算乘积需要重复访问子矩阵中的值,因此总体性能提升。 对于每种算法,需要试验才能找到最佳算法和平铺大小;

在非 AMP 和非平铺示例中,从全局内存访问 A 和 B 的每个元素四次,以计算乘积。 在平铺示例中,每个元素从全局内存访问两次,并从 tile_static 内存访问四次。 这不是显著的性能提升。 但是,如果 A 和 B 是 1024x1024 矩阵,平铺大小为 16,则性能将显著提升。 在这种情况下,每个元素将仅复制到 tile_static 内存中 16 次,并从 tile_static 内存访问 1024 次。

2. 如下所示修改 main 方法以调用 MultiplyWithTiling 方法。

int main() {
    MultiplyWithOutAMP();
    MultiplyWithAMP();
    MultiplyWithTiling();
    getchar();
}

编译并运行;

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值