共用方式為


逐步解說:偵錯C++ AMP 應用程式

本文示範如何偵錯使用C++加速大規模平行處理原則 (C++ AMP) 來利用圖形處理單元 (GPU) 的應用程式。 它會使用平行縮減程式來加總大量的整數陣列。 本逐步解說將說明下列工作:

  • 啟動 GPU 調試程式。
  • 在 [GPU 線程] 視窗中檢查 GPU 線程。
  • 使用 [ 平行堆棧 ] 視窗,同時觀察多個 GPU 線程的呼叫堆疊。
  • 使用 [ 平行監看 式] 視窗,同時檢查跨多個線程的單一表達式值。
  • 標記、凍結、解除凍結和群組 GPU 線程。
  • 將磚的所有線程執行到程序代碼中的特定位置。

必要條件

開始本逐步解說之前:

注意

從 Visual Studio 2022 17.0 版開始,C++ AMP 標頭已被取代。 包含任何 AMP 標頭將會產生建置錯誤。 先定義 _SILENCE_AMP_DEPRECATION_WARNINGS ,再包含任何 AMP 標頭以讓警告無聲。

  • 閱讀 C++ AMP 概觀
  • 請確定文字編輯器中會顯示行號。 如需詳細資訊,請參閱 如何:在編輯器中顯示行號。
  • 請確定您至少執行 Windows 8 或 Windows Server 2012,以支援軟體模擬器上的偵錯。

注意

在下列指示的某些 Visual Studio 使用者介面項目中,您的電腦可能會顯示不同的名稱或位置: 您所擁有的 Visual Studio 版本以及使用的設定會決定這些項目。 如需詳細資訊,請參閱將 Visual Studio IDE 個人化

建立範例專案

建立專案的指示會根據您使用的 Visual Studio 版本而有所不同。 請確定您已在此頁面上選取正確的檔版本。

在 Visual Studio 中建立範例專案

  1. 在功能表列上,選擇 [檔案]>[新增]>[專案],以開啟 [建立新專案] 對話方塊。

  2. 在對話方塊頂端,將 [語言] 設定為 C++,將 [平台] 設定為 Windows,並將 [專案類型] 設定為主控台

  3. 從專案類型的篩選清單中,選擇 [主控台應用程式],然後選擇 [下一步]。 在下一頁中,於 [名稱] 方塊中輸入 AMPMapReduce ,以指定項目的名稱,並視需要不同的專案位置指定專案位置。

    顯示 [建立新專案] 對話框的螢幕快照,其中已選取 [主控台應用程式] 範本。

  4. 選擇 [建立] 按鈕以建立用戶端專案。

在 Visual Studio 2017 或 Visual Studio 2015 中建立範例專案

  1. 啟動 Visual Studio。

  2. 在功能表列上,選擇 [檔案]>[新增]>[專案]

  3. [範本] 窗格中的 [已安裝 ] 下,選擇 [Visual C++]。

  4. 選擇 [Win32 控制台應用程式],在 [名稱] 方塊中輸入 AMPMapReduce ,然後選擇 [確定] 按鈕。

  5. 選擇 [下一步] 按鈕。

  6. 清除 [ 先行編譯標頭 ] 複選框,然後選擇 [ 完成] 按鈕。

  7. 方案總管 中,從專案刪除 stdafx.h、targetver.hstdafx.cpp

下一步:

  1. 開啟AMPMapReduce.cpp,並以下列程序代碼取代其內容。

    // AMPMapReduce.cpp defines the entry point for the program.
    // The program performs a parallel-sum reduction that computes the sum of an array of integers.
    
    #include <stdio.h>
    #include <tchar.h>
    #include <amp.h>
    
    const int BLOCK_DIM = 32;
    
    using namespace concurrency;
    
    void sum_kernel_tiled(tiled_index<BLOCK_DIM> t_idx, array<int, 1> &A, int stride_size) restrict(amp)
    {
        tile_static int localA[BLOCK_DIM];
    
        index<1> globalIdx = t_idx.global * stride_size;
        index<1> localIdx = t_idx.local;
    
        localA[localIdx[0]] =  A[globalIdx];
    
        t_idx.barrier.wait();
    
        // Aggregate all elements in one tile into the first element.
        for (int i = BLOCK_DIM / 2; i > 0; i /= 2)
        {
            if (localIdx[0] < i)
            {
    
                localA[localIdx[0]] += localA[localIdx[0] + i];
            }
    
            t_idx.barrier.wait();
        }
    
        if (localIdx[0] == 0)
        {
            A[globalIdx] = localA[0];
        }
    }
    
    int size_after_padding(int n)
    {
        // The extent might have to be slightly bigger than num_stride to
        // be evenly divisible by BLOCK_DIM. You can do this by padding with zeros.
        // The calculation to do this is BLOCK_DIM * ceil(n / BLOCK_DIM)
        return ((n - 1) / BLOCK_DIM + 1) * BLOCK_DIM;
    }
    
    int reduction_sum_gpu_kernel(array<int, 1> input)
    {
        int len = input.extent[0];
    
        //Tree-based reduction control that uses the CPU.
        for (int stride_size = 1; stride_size < len; stride_size *= BLOCK_DIM)
        {
            // Number of useful values in the array, given the current
            // stride size.
            int num_strides = len / stride_size;
    
            extent<1> e(size_after_padding(num_strides));
    
            // The sum kernel that uses the GPU.
            parallel_for_each(extent<1>(e).tile<BLOCK_DIM>(), [&input, stride_size] (tiled_index<BLOCK_DIM> idx) restrict(amp)
            {
                sum_kernel_tiled(idx, input, stride_size);
            });
        }
    
        array_view<int, 1> output = input.section(extent<1>(1));
        return output[0];
    }
    
    int cpu_sum(const std::vector<int> &arr) {
        int sum = 0;
        for (size_t i = 0; i < arr.size(); i++) {
            sum += arr[i];
        }
        return sum;
    }
    
    std::vector<int> rand_vector(unsigned int size) {
        srand(2011);
    
        std::vector<int> vec(size);
        for (size_t i = 0; i < size; i++) {
            vec[i] = rand();
        }
        return vec;
    }
    
    array<int, 1> vector_to_array(const std::vector<int> &vec) {
        array<int, 1> arr(vec.size());
        copy(vec.begin(), vec.end(), arr);
        return arr;
    }
    
    int _tmain(int argc, _TCHAR* argv[])
    {
        std::vector<int> vec = rand_vector(10000);
        array<int, 1> arr = vector_to_array(vec);
    
        int expected = cpu_sum(vec);
        int actual = reduction_sum_gpu_kernel(arr);
    
        bool passed = (expected == actual);
        if (!passed) {
            printf("Actual (GPU): %d, Expected (CPU): %d", actual, expected);
        }
        printf("sum: %s\n", passed ? "Passed!" : "Failed!");
    
        getchar();
    
        return 0;
    }
    
  2. 在功能表列上,依序選擇 [檔案]>[全部儲存]

  3. 方案總管 中,開啟 AMPMapReduce快捷方式功能表,然後選擇 [屬性]。

  4. 在 [屬性頁] 對話框的 [組態屬性] 下,選擇 [C/C++>][編譯標頭]。

  5. 針對 [先行編譯標頭] 屬性,選取 [不使用先行編譯標頭],然後選擇 [確定] 按鈕。

  6. 在功能表列上選擇 [建置]>[建置解決方案]

偵錯 CPU 程式代碼

在此程式中,您將使用本機 Windows 調試程式來確保此應用程式中的 CPU 程式代碼正確無誤。 此應用程式中 CPU 程式代碼的區段特別有趣,就是 for 函式中的 reduction_sum_gpu_kernel 迴圈。 它會控制在 GPU 上執行的樹狀結構型平行縮減。

偵錯 CPU 程式代碼

  1. 方案總管 中,開啟 AMPMapReduce快捷方式功能表,然後選擇 [屬性]。

  2. 在 [屬性頁] 對話框的 [組態屬性] 底下,選擇 [偵錯]。 確認已在 [調試程式] 中選取 [要啟動的本機 Windows 調試程式] 清單。

  3. 返回程式 代碼編輯器

  4. 在下圖所示的程式代碼行上設定斷點(大約第 67 行 70 行)。

    編輯器中程式代碼行旁標示的CPU斷點。
    CPU 中斷點

  5. 在功能表列上,依序選擇 [偵錯]>[開始偵錯]

  6. 在 [ 局部變數 ] 視窗中,觀察 的值 stride_size ,直到到達第 70 行的斷點為止。

  7. 在功能表列上,依序選擇 [偵錯]>[停止偵錯]

偵錯 GPU 程式代碼

本節說明如何偵錯 GPU 程式代碼,這是函式中包含的 sum_kernel_tiled 程序代碼。 GPU 程式代碼會平行計算每個「區塊」的整數總和。

偵錯 GPU 程式代碼

  1. 方案總管 中,開啟 AMPMapReduce快捷方式功能表,然後選擇 [屬性]。

  2. 在 [屬性頁] 對話框的 [組態屬性] 底下,選擇 [偵錯]。

  3. 在 [要啟動的偵錯工具] 清單中,選取 [本機 Windows 偵錯工具]

  4. 在 [ 調試程序類型] 列表中,確認已 選取 [自動 ]。

    Auto 是預設值。 在 Windows 10 之前的版本中, GPU 僅限 是必要值,而不是 Auto

  5. 選擇 [確定] 按鈕。

  6. 在第 30 行設定斷點,如下圖所示。

    編輯器中程式代碼行旁標示的 GPU 斷點。
    GPU 斷點

  7. 在功能表列上,依序選擇 [偵錯]>[開始偵錯]。 CPU 程式代碼在第 67 行和 70 行的斷點不會在 GPU 偵錯期間執行,因為這些程式代碼行會在 CPU 上執行。

使用 [GPU 線程] 視窗

  1. 若要開啟 [GPU 線程] 視窗,請在功能表欄上,選擇 [偵>錯 Windows>GPU 線程]。

    您可以在出現的 [GPU 線程] 視窗中檢查 GPU 線程的狀態。

  2. [GPU 線程] 視窗停駐在 Visual Studio 底部。 選擇 [ 展開線程切換 ] 按鈕以顯示磚和線程文字框。 [ GPU 線程] 視窗會顯示作用中和封鎖的 GPU 線程總數,如下圖所示。

    具有 4 個作用中線程的 GPU 線程視窗。
    [GPU 執行緒] 視窗

    313 個磚會配置給此計算。 每個圖格都包含32個線程。 由於本機 GPU 偵錯發生在軟體模擬器上,因此有四個作用中的 GPU 線程。 這四個線程會同時執行指令,然後一起移至下一個指令。

    在 [GPU 線程] 視窗中,在大約第 21t_idx.barrier.wait(); 行定義的 tile_barrier::wait 語句中,有四個 GPU 線程作用中,28 個 GPU 線程遭到封鎖。 所有 32 個 GPU 線程都屬於第一個磚 tile[0]。 箭號指向包含目前線程的數據列。 若要切換至不同的線程,請使用下列其中一種方法:

    • 在線程切換至 [GPU 線程 ] 視窗中的數據列中,開啟快捷方式功能表,然後選擇 [ 切換至線程]。 如果數據列代表一個以上的線程,您將根據線程座標切換至第一個線程。

    • 在對應的文字框中輸入線程的圖格和線程值,然後選擇 [ 切換線程] 按鈕。

    [ 呼叫堆疊 ] 視窗會顯示目前 GPU 線程的呼叫堆疊。

使用 [平行堆疊] 視窗

  1. 若要開啟 [平行堆棧] 視窗,請在功能表欄上,選擇 [偵>錯 Windows>平行堆棧]。

    您可以使用 [ 平行堆棧 ] 視窗,同時檢查多個 GPU 線程的堆疊框架。

  2. [平行堆棧] 視窗停駐在 Visual Studio 底部。

  3. 請確定 已在左上角的清單中選取 [線程 ]。 在下圖中,[ 平行堆棧 ] 視窗會顯示您在 [GPU 線程] 視窗中看到 之 GPU 線程 的呼叫堆棧焦點檢視。

    具有 4 個作用中線程的平行堆疊視窗。
    [平行堆疊] 視窗

    32 個線程會從函式呼叫中的 parallel_for_each Lambda 語句,然後移至_kernel_stubsum_kernel_tiled函式,其中會發生平行縮減。 32 個線程中有 28 個已進展至 語句, tile_barrier::wait 並在第 22 行保持封鎖,而其他四個線程則保留在函式中 sum_kernel_tiled ,第 30 行。

    您可以檢查 GPU 線程的屬性。 其可在 [平行堆棧] 視窗的豐富數據提示的 [GPU 線程] 視窗中取得。 若要查看它們,請將指標暫留在的 sum_kernel_tiled堆疊框架上。 下圖顯示DataTip。

    [平行堆疊] 視窗的數據提示。
    GPU 線程數據提示

    如需平行堆疊視窗的詳細資訊,請參閱使用平行堆棧視窗

使用 [平行監看式] 視窗

  1. 若要開啟 [平行監看式] 視窗,請在功能表欄上,選擇 [偵>錯 Windows>平行監看式平行監看>式 1]。

    您可以使用 [ 平行監看 式] 視窗來檢查跨多個線程的運算式值。

  2. [平行監看式 1] 視窗停駐在 Visual Studio 底部。 [平行監看式] 視窗數據表中有 32 個數據列。 每個都對應至 GPU 線程視窗和 平行堆疊 視窗中顯示的 GPU 線程。 現在,您可以輸入想要在所有 32 個 GPU 線程中檢查其值的運算式。

  3. 選取 [新增監看式] 數據行標頭,輸入 ,然後選擇Enter localIdx 鍵。

  4. 再次選取 [ 新增監看 式] 數據行標頭,輸入 globalIdx,然後選擇 Enter 鍵。

  5. 再次選取 [ 新增監看 式] 數據行標頭,輸入 localA[localIdx[0]],然後選擇 Enter 鍵。

    您可以藉由選取其對應的數據行標頭,依指定的表達式排序。

    選取 localA[localIdx[0]] 資料行標頭來排序數據行。 下圖顯示依 localA[localIdx[0]] 排序的結果。

    具有已排序結果的 [平行監看式] 視窗。
    排序結果

    您可以選擇 [Excel] 按鈕,然後選擇 [在 Excel 中開啟],將 [平行監看式] 視窗中的內容匯出至 Excel。 如果您已在開發計算機上安裝 Excel,按鈕會開啟包含內容的 Excel 工作表。

  6. 在 [平行監看式] 視窗右上角,有一個篩選控件,您可以使用布爾表達式來篩選內容。 在篩選控件文本框中輸入 localA[localIdx[0]] > 20000 ,然後選擇 Enter 鍵。

    窗口現在只包含值大於 20000 的 localA[localIdx[0]] 線程。 內容仍會依 localA[localIdx[0]] 數據行排序,也就是您稍早選擇的排序動作。

標記 GPU 線程

您可以在 [GPU 線程] 視窗、[平行監看式] 視窗或 [平行堆棧] 視窗中的 [數據提示] 視窗中標記特定 GPU 線程。 如果 [GPU 線程] 視窗中的數據列包含一個以上的線程,則標示該數據列會標幟數據列中包含的所有線程。

為 GPU 線程加上旗標

  1. 在 [平行監看式 1] 視窗中選取 [線程] 數據行標頭,依磚索引和線程索引排序。

  2. 在功能表欄上,選擇 >[偵錯繼續],這會導致作用中的四個線程進入下一個屏障(定義於第 32 行AMPMapReduce.cpp)。

  3. 選擇數據列左側的旗標符號,其中包含目前作用中的四個線程。

    下圖顯示 [GPU 線程] 視窗中的四個作用中 標幟線程

    具有標幟線程的 GPU 線程視窗。
    [GPU 執行緒] 視窗中正在活動的執行緒

    [平行監看式] 視窗和 [平行堆棧] 視窗的 [數據提示] 都表示已標幟的線程。

  4. 如果您想要將焦點放在標示的四個線程上,您可以選擇只顯示已標示的線程。 它會限制您在 GPU 線程、平行監看式和平行堆疊視窗中看到的內容。

    選擇任何視窗或 [偵錯位置] 工具列上的 [僅顯示標幟] 按鈕。 下圖顯示 [偵錯位置] 工具列上的 [僅顯示標幟] 按鈕。

    [僅顯示標幟] 圖示的 [偵錯位置] 工具列。
    顯示 [僅標幟] 按鈕

    現在,GPU 線程平行監看平行堆疊視窗只會顯示標幟的線程。

凍結和解除凍結 GPU 線程

您可以從 [GPU 線程] 視窗或 [平行監看式] 視窗凍結 (暫停) 和解除凍結 (繼續) GPU 線程。 您可以凍結和解除凍結 CPU 線程的方式相同;如需詳細資訊,請參閱 如何:使用線程視窗

凍結和解除凍結 GPU 線程

  1. 選擇 [ 僅顯示標幟] 按鈕以顯示所有線程。

  2. 在功能表欄上,選擇 [偵錯>繼續]。

  3. 開啟使用中數據列的快捷方式功能表,然後選擇 [ 凍結]。

    [GPU 線程] 視窗的下圖顯示所有四個線程都已凍結。

    顯示凍結線程的 GPU 線程視窗。
    GPU 線程視窗中的凍結線程

    同樣地,[ 平行監看 式] 視窗會顯示所有四個線程都已凍結。

  4. 在功能表欄上,選擇 >[偵錯繼續] 以允許接下來的四個 GPU 線程經過第 22 行的屏障,並在第 30 行到達斷點。 [ GPU 線程] 視窗會顯示四個先前凍結的線程會維持凍結狀態,且處於作用中狀態。

  5. 在功能表欄上,選擇 [偵錯]、[繼續]。

  6. 從 [ 平行監看 式] 視窗中,您也可以解除凍結個別或多個 GPU 線程。

將 GPU 線程分組

  1. 在 [GPU 線程] 視窗中其中一個線程的快捷方式功能表上,選擇 [分組依據]、[位址]。

    [GPU 線程] 視窗中的線程會依位址分組。 地址會對應至反組譯碼中每個線程群組所在的指令。 24 個線程位於執行tile_barrier::wait 方法的第 22 行。 12 個線程位於第 32 行屏障的指示中。 其中四個線程會標幟。 八個線程位於第 30 行的斷點。 其中四個線程已凍結。 下圖顯示 [GPU 線程] 視窗中的群組線程

    [GPU 線程] 視窗,其線程會依 [位址] 分組。
    GPU 線程視窗中的群組線程

  2. 您也可以開啟 [平行監看式] 視窗數據格的快捷方式功能表,以執行 [分組依據] 作業。 選取 [ 群組依據],然後選擇對應至您要如何群組線程的功能表項。

將所有線程執行至程序代碼中的特定位置

您可以使用 [執行目前磚至資料指標],將指定磚中的所有線程執行到包含游標的行。

若要將所有線程執行至數據指標所標示的位置

  1. 在凍結線程的快捷方式功能表上,選擇 [解除凍結]。

  2. 在程式代碼編輯器,將游標放在第 30 行。

  3. 在 [程序代碼編輯器] 的快捷方式功能表上,選擇 [執行目前磚至游標]。

    先前在第 21 行的屏障上封鎖的 24 個線程已進入第 32 行。 它會顯示在 [GPU 線程] 視窗中。

另請參閱

C++ AMP 概觀
偵錯 GPU 程式代碼
如何:使用 GPU 執行緒視窗
如何:使用平行監看式視窗
使用並行可視化檢視分析C++ AMP 程序代碼