演练:矩阵乘法

本分步演练演示如何使用 C++ AMP 加快矩阵相乘的执行。提供两种算法,一个不拼贴的情况下,一个使用平铺。

系统必备

在开始之前:

  • 读取 C++ AMP 概述

  • 读取 使用平铺

  • 请确保Windows 7, Windows 8, Windows Server 2008 R2,或Windows Server 2012安装在您的计算机上。

创建项目

  1. 在 Visual Studio 中菜单栏中,选择文件New项目

  2. 已安装 在模板窗格中选择 Visual C++

  3. 选择空项目,输入 MatrixMultiply 在名称 ,单击然后选择 确定按钮。

  4. 选择下一步按钮。

  5. 解决方案资源管理器,打开快捷菜单的 源文件,然后选择 添加新项

  6. 添加新项 对话框中,选择 C++ 文件 (.cpp),输入 MatrixMultiply.cpp 在名称 ,单击然后选择 添加按钮。

乘法不拼贴

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

3x2 矩阵2x3 矩阵

3-2 的矩阵就是 A 和 B 是 2-3 矩阵。放大 A b 的产品是以下 3 x 3 矩阵。产品乘以 B 元素通过元素的列 A 中的行。

3x3 矩阵

将不使用 C++ 的存储系统

  1. 打开 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";
        }
    }
    
    void main() {
        MultiplyWithOutAMP();
        getchar();
    }
    

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

  2. 在菜单栏中,选择文件全部保存

  3. 选择键盘快捷键 F5 开始调试和验证输出正确。

  4. 选择退出该应用程序的输入。

要通过使用 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";
        }
    }
    

    存储系统的代码类似于非存储系统的代码。调用parallel_for_each启动一个线程中每个元素product.extent,并替换for的行和列的循环。处的行和列的单元格的值是在idx。您可以访问的元素的array_view对象使用[]运算符和一个索引的变量,或()运算符,这些行和列的变量。该示例演示这两种方法。array_view::synchronize方法将复制的值product到变量productMatrix变量。

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

    #include <amp.h>
    using namespace concurrency;
    
  3. 修改main调用方法MultiplyWithAMP方法。

    void main() {
        MultiplyWithOutAMP();
        MultiplyWithAMP();
        getchar();
    }
    
  4. 选择开始调试的 Ctrl + F5 键盘快捷方式,然后验证输出正确。

  5. 选择空格键退出该应用程序。

乘法的拼贴

平铺是一种技术,数据分区分成相等的子集,称作tiles。以下三项更改时,使用平铺。

  • 您可以创建tile_static变量。在数据访问tile_static空间可以是全局空间中数据的访问比快许多倍。实例的tile_static变量,将创建的每个图块,并在图块中的所有线程都有权访问该变量。平铺的主要好处是性能增益,由于tile_static访问。

  • 您可以调用 tile_barrier::wait 方法来停止所有的线程中一个拼贴在指定的代码行。您无法保证线程仅运行的顺序所有在一个拼贴中的线程将停止在调用tile_barrier::wait在继续执行之前。

  • 您有权访问的线程相对于整个索引array_view对象,相对于该图块的索引。通过使用本地的索引,可以使代码更易于阅读和调试。

要利用在矩阵相乘的拼贴,算法必须矩阵划分为拼贴,然后将复制到的图块数据tile_static变量,以提高访问速度。在此示例中,该矩阵分区为多个大小相等的 submatrices。通过将 submatrices 找到产品。两个矩阵相加和他们的产品,在本例中为:

4x4 矩阵4x4 矩阵4x4 矩阵

矩阵划分到四个 2x2 矩阵,其定义,如下所示:

已划分为 2x2 子矩阵的 4x4 矩阵已划分为 2x2 子矩阵的 4x4 矩阵

该产品的 A 和 B 现在可以编写并计算公式如下:

已划分为 2x2 子矩阵的 4x4 矩阵

因为矩阵a到h是 2x2 矩阵,所有的产品,它们的总和也是 2x2 矩阵。它也遵循 A * B 是 4x4 矩阵,像预期的那样。若要快速检查算法,计算的第一行中的元素,在产品中的第一列的值。在本例中,这可能是该元素的值中的第一行和第一列的ae + bg。您只需计算第一列、 第一行的ae和bg的每个术语。That value for ae is 1*1 + 2*5 = 11.bg 的值为 3*1 + 4*5 = 23。最终值是11 + 23 = 34,这是正确。

若要实现这种算法,该代码:

  • 使用tiled_extent对象而不是extent对象中parallel_for_each调用。

  • 使用tiled_index对象而不是index对象中parallel_for_each调用。

  • 创建tile_static变量来保存 submatrices。

  • 使用tile_barrier::wait若要停止的线程为 submatrices 的乘积的计算方法。

进行乘法运算使用存储系统和拼贴

  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";
        }
    
    }
    

    此示例与示例平铺没有显著不同。该代码使用这些概念的步骤:

    1. 复制元素的平铺 [0,0] 的a到locA。复制元素的平铺 [0,0] 的b到locB。请注意, product平铺,没有a和b。因此,您使用全局索引来访问a, b,和product。在调用tile_barrier::wait是必不可少的。停止所有拼贴中的线程不动直到两个locA和locB进行填充。

    2. 乘locA和locB ,并将结果放product。

    3. 复制元素的平铺 [0,1] 的a到locA。复制元素的平铺 [1,0] 的b到locB。

    4. 乘locA和locB ,并将它们添加到的结果是已在product。

    5. 平铺 [0,0] 的乘法已完成。

    6. 其他四个拼贴的重复。没有专门为拼贴没有编制索引,并且线程可以按任意顺序执行。在每个线程执行, tile_static变量的相应地创建每个拼贴和调用tile_barrier::wait程序流。

    7. 在仔细检查该算法,请注意每个 submatrix 被加载到tile_static内存的两倍。该数据传输确实需要一些时间。但是,一旦数据进入tile_static的内存,对数据的访问是要快得多。计算产品需要重复的访问 submatrices 中的值,因为没有整体性能增益。为每个算法,试验才能找到最佳的算法和拼贴大小。

      在非存储系统和非平铺示例中,每个元素的 A 和 B 从计算产品的全局内存访问四倍。在平铺的示例中,每个元素访问两次从全局内存和四次tile_static内存。这不是极大地提高性能。但是,如果 A 和 B 是 1024 x 1024 的矩阵和拼贴大小是 16,会有显著的性能提升。在这种情况下,每个元素被复制到tile_static内存只有 16 倍,并可从访问tile_static内存 1024年倍。

  2. 修改主方法调用 MultiplyWithTiling 方法,如所示。

    void main() {
        MultiplyWithOutAMP();
        MultiplyWithAMP();
        MultiplyWithTiling();
        getchar();
    }
    
  3. 选择开始调试的 Ctrl + F5 键盘快捷方式,然后验证输出正确。

  4. 选择空格键退出该应用程序。

请参见

任务

演练:调试 C++ AMP 应用程序

其他资源

C++ AMP (C++ Accelerated Massive Parallelism)