Passo a passo: Depurar um aplicativo C++ AMP

Este artigo demonstra como depurar um aplicativo que usa C++ Accelerated Massive Parallelism (C++ AMP) para aproveitar a unidade de processamento de elementos gráficos (GPU). Ele usa um programa de redução paralela que soma uma grande matriz de inteiros. Este passo a passo ilustra as seguintes tarefas:

  • Iniciar o depurador da GPU.
  • Inspecionar threads de GPU na janela Threads da GPU.
  • Usar a janela Pilhas Paralelas para observar simultaneamente as pilhas de chamadas de vários threads da GPU.
  • Usar a janela Inspeção Paralela para inspecionar valores de uma única expressão em vários threads ao mesmo tempo.
  • Sinalizar, congelar, descongelar e agrupar threads da GPU.
  • Executar todos os threads de um bloco para um local específico no código.

Pré-requisitos

Leia sobre o Fluxo de dados antes de iniciar essa explicação passo a passo:

Observação

Os cabeçalhos AMP C++ foram preteridos a partir do Visual Studio 2022 versão 17.0. Incluir todos os cabeçalhos AMP gerará erros de build. Defina _SILENCE_AMP_DEPRECATION_WARNINGS antes de incluir qualquer cabeçalho AMP para silenciar os avisos.

  • Leia Visão Geral do C++ AMP.
  • Verifique se os números de linha são exibidos no editor de texto. Para obter mais informações, consulte Como exibir números de linha no editor.
  • Verifique se você está executando pelo menos Windows 8 ou Windows Server 2012 para dar suporte à depuração no emulador de software.

Observação

Seu computador pode mostrar diferentes nomes ou locais para alguns dos elementos de interface do usuário do Visual Studio nas instruções a seguir. A edição do Visual Studio que você possui e as configurações que você usa determinam esses elementos. Para obter mais informações, consulte Personalizando o IDE.

Para criar o projeto de exemplo

As instruções para criar um projeto variam dependendo de qual versão do Visual Studio você está usando. Verifique se você tem a versão correta da documentação selecionada acima do sumário nesta página.

Para criar o projeto no Visual Studio

  1. Na barra de menus, escolha Arquivo>Novo>Projeto para abrir a caixa de diálogo Criar um projeto.

  2. Na parte superior da caixa de diálogo, defina Linguagem como C++, Plataforma como Windows e Tipo de projeto como Console.

  3. Na lista filtrada de tipos de projeto, escolha Aplicativo de Console e, em seguida, escolha Avançar. Na próxima página, insira AMPMapReduce na caixa Nome para especificar um nome para o projeto e, se quiser, especifique um diferente.

    Screenshot showing the Create a new project dialog with the Console App template selected.

  4. Escolha o botão Criar para criar o projeto do cliente.

Para criar o projeto de exemplo no Visual Studio 2017 ou no Visual Studio 2015

  1. Inicie o Visual Studio.

  2. Na barra de menus, escolha Arquivo>Novo>Projeto.

  3. Em Instalado no painel modelos, escolha Visual C++.

  4. Escolha Aplicativo de Console do Win32, digite AMPMapReduce na caixa Nome e escolha o botão OK.

  5. Escolha o botão Avançar.

  6. Desmarque a caixa de seleção de cabeçalho pré-compilado e escolha o botão Concluir.

  7. No Gerenciador de Soluções, exclua stdafx.h, targetver.h e stdafx.cpp do projeto.

Avançar:

  1. Abra AMPMapReduce.cpp e substitua seu conteúdo pelo seguinte código.

    // 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. Na barra de menus, escolha Arquivo>Salvar Todos.

  3. No Gerenciador de Soluções, abra o menu de atalho para AMPMapReduce e escolha Propriedades.

  4. Na caixa de diálogo Páginas de Propriedades, em Propriedades de Configuração, escolha C/C++>Cabeçalhos pré-compilados.

  5. Para a propriedade Cabeçalho pré-compilado, selecione Não usar cabeçalhos pré-compilados e escolha o botão OK.

  6. Na barra de menus, escolha Compilar>Compilar Solução.

Depurando o código da CPU

Neste procedimento, você usará o Depurador Local do Windows para verificar se o código da CPU neste aplicativo está correto. O segmento do código da CPU neste aplicativo que é especialmente interessante é o loop for na função reduction_sum_gpu_kernel. Ele controla a redução paralela baseada em árvore que é executada na GPU.

Para depurar o código da CPU

  1. No Gerenciador de Soluções, abra o menu de atalho para AMPMapReduce e escolha Propriedades.

  2. Na caixa de diálogo Páginas de Propriedades, em Propriedades de Configuração, selecione Depuração. Verifique se o Depurador Local do Windows está selecionado na lista Depurador a ser inicializado.

  3. Retorne ao Editor de códigos.

  4. Defina pontos de interrupção nas linhas de código mostradas na ilustração a seguir (aproximadamente linhas 67 linha 70).

    CPU breakpoints marked next to lines of code in the editor.
    Pontos de interrupção da CPU

  5. Na barra de menus, escolha Depurar>Iniciar Depuração.

  6. Na janela Locais, observe o valor de stride_size até que o ponto de interrupção na linha 70 seja atingido.

  7. Na barra de menus, escolha Depurar>Parar Depuração.

Depurando o código da GPU

Esta seção mostra como depurar o código da GPU, que é o código contido na função sum_kernel_tiled. O código da GPU calcula a soma de inteiros para cada "bloco" em paralelo.

Para depurar o código da GPU

  1. No Gerenciador de Soluções, abra o menu de atalho para AMPMapReduce e escolha Propriedades.

  2. Na caixa de diálogo Páginas de Propriedades, em Propriedades de Configuração, selecione Depuração.

  3. Na lista Depurador a iniciar, selecione Depurador Local do Windows.

  4. Na lista Tipo de depurador, verifique se a opção Auto está selecionada.

    Auto é o valor padrão. Em versões antes de Windows 10, Somente GPU é o valor necessário em vez de Auto.

  5. Clique no botão OK.

  6. Defina um ponto de interrupção na linha 30, conforme mostrado na ilustração a seguir.

    GPU breakpoints marked next to a line of code in the editor.
    Ponto de interrupção da GPU

  7. Na barra de menus, escolha Depurar>Iniciar Depuração. Os pontos de interrupção no código da CPU, nas linhas 67 e 70, não são executados durante a depuração da GPU porque essas linhas de código são executadas na CPU.

Para usar a janela Threads da GPU

  1. Para abrir a janela Threads da GPU, na barra de menus, escolha Depurar>Windows>Threads da GPU.

    Você pode inspecionar o estado dos threads da GPU na janela Threads da GPU exibida.

  2. Encaixe a janela Threads da GPU na parte inferior do Visual Studio. Escolha o botão Expandir Opção de Thread para exibir as caixas de texto do bloco e do thread. A janela Threads da GPU mostra o número total de threads ativos e bloqueados da GPU, conforme mostrado na ilustração a seguir.

    GPU Threads window with 4 active threads.
    Janela Threads de GPU

    313 blocos são alocados para essa computação. Cada bloco contém 32 threads. Como a depuração da GPU local ocorre em um emulador de software, há quatro threads de GPU ativos. Os quatro threads executam as instruções simultaneamente e avançam juntos para a próxima instrução.

    Na janela Threads da GPU, há quatro threads de GPU ativos e 28 threads de GPU bloqueados na instrução tile_barrier::wait definida na linha 21 (t_idx.barrier.wait();). Todos os 32 threads de GPU pertencem ao primeiro bloco. tile[0] Uma seta aponta para a linha que inclui o thread atual. Para alternar para um thread diferente, use um dos seguintes métodos:

    • Na linha para a qual o thread alternar na janela Threads da GPU, abra o menu de atalho e escolha Alternar para Thread. Se a linha representar mais de um thread, você alternará para o primeiro thread de acordo com as coordenadas do thread.

    • Insira os valores de bloco e thread do thread nas caixas de texto correspondentes e escolha o botão Alternar Thread.

    A janela Pilha de chamadas exibe a pilha de chamadas do thread de GPU atual.

Para usar a janela Pilhas paralelas

  1. Para abrir a janela Pilhas paralelas, na barra de menus, escolha Depurar>Windows>Pilhas paralelas.

    Você pode usar a janela Pilhas paralelas para inspecionar simultaneamente os registros de ativação de vários threads de GPU.

  2. Encaixe a janela Pilhas paralelas na parte inferior do Visual Studio.

  3. Verifique se Threads está selecionado na lista no canto superior esquerdo. Na ilustração a seguir, a janela Pilhas paralelas mostra uma exibição focada na pilha de chamadas dos threads de GPU que você viu na janela Threads da GPU.

    Parallel Stacks window with 4 active threads.
    Janela Pilhas Paralelas

    32 threads foram da instrução _kernel_stub para a instrução lambda na chamada da função parallel_for_eache, em seguida, para a função sum_kernel_tiled, onde ocorre a redução paralela. 28 dos 32 threads progrediram para a instrução tile_barrier::wait e permanecem bloqueados na linha 22, enquanto os outros quatro threads permanecem ativos na função sum_kernel_tiled na linha 30.

    Você pode inspecionar as propriedades de um thread de GPU. Eles estão disponíveis na janela Threads de GPU na DataTip avançada da janela Pilhas Paralelas. Para vê-los, passe o ponteiro sobre o registro de ativação de sum_kernel_tiled. A ilustração a seguir mostra a DataTip.

    DataTip for Parallel Stacks window.
    DataTip do thread da GPU

    Para obter mais informações sobre a janela Pilhas Paralelas, consulte Usando a janela Pilhas Paralelas.

Para usar a janela Inspeção Paralela

  1. Para abrir a janela Inspeção Paralela, na barra de menus, escolha Depurar> Windows>Inspeção Paralela>Inspeção Paralela 1.

    Você pode usar a janela Inspeção Paralela para inspecionar os valores de uma expressão em vários threads.

  2. Encaixe a janela Inspeção Paralela 1 na parte inferior do Visual Studio. Há 32 linhas na tabela da janela Inspeção Paralela. Cada uma corresponde a um thread de GPU que apareceu na janela Threads da GPU e na janela Pilhas Paralelas. Agora, você pode inserir expressões cujos valores deseja inspecionar em todos os 32 threads de GPU.

  3. Selecione o cabeçalho da coluna Adicionar inspeção, insira localIdxe escolha a tecla Enter.

  4. Selecione o cabeçalho da coluna Adicionar inspeção novamente, digite globalIdx e escolha a tecla Enter.

  5. Selecione o cabeçalho da coluna Adicionar inspeção novamente, digite localA[localIdx[0]] e escolha a tecla Enter.

    Você pode classificar por uma expressão especificada selecionando seu cabeçalho de coluna correspondente.

    Selecione o cabeçalho da coluna localA[localIdx[0]] para classificar a coluna. A ilustração a seguir mostra os resultados da classificação por localA[localIdx[0]].

    Parallel Watch window with sorted results.
    Resultados da classificação

    Você pode exportar o conteúdo na janela Inspeção Paralela para o Excel escolhendo o botão do Excel e, em seguida, escolhendo Abrir no Excel. Se você tiver o Excel instalado no computador de desenvolvimento, o botão abrirá uma planilha do Excel que contém o conteúdo.

  6. No canto superior direito da janela Inspeção Paralela, há um controle de filtro que você pode usar para filtrar o conteúdo usando expressões boolianas. Insira localA[localIdx[0]] > 20000 na caixa de texto de controle de filtro e escolha a tecla Enter.

    A janela agora contém apenas threads nos quais o valor localA[localIdx[0]] é maior que 20000. O conteúdo ainda é classificado pela coluna localA[localIdx[0]], que é a ação de classificação escolhida anteriormente.

Sinalizando threads de GPU

Você pode marcar threads de GPU específicos sinalizando-os na janela Threads da GPU, na janela Inspeção Paralela ou na DataTip na janela Pilhas Paralelas. Se uma linha na janela threads de GPU contiver mais de um thread, sinalizando que a linha sinaliza todos os threads contidos na linha.

Para sinalizar threads de GPU

  1. Selecione o cabeçalho da coluna [Thread] na janela Inspeção Paralela 1 para classificar por índice de bloco e índice de thread.

  2. Na barra de menus, escolha Depurar>Continuar, o que faz com que os quatro threads que estavam ativos progridam para a próxima barreira (definida na linha 32 de AMPMapReduce.cpp).

  3. Escolha o símbolo de sinalizador no lado esquerdo da linha que contém os quatro threads que agora estão ativos.

    A ilustração a seguir mostra os quatro threads sinalizados ativos na janela Threads da GPU.

    GPU Threads window with flagged threads.
    Threads ativos na janela Threads da GPU

    A janela Inspeção Paralela e a DataTip da janela Pilhas Paralelas indicam os threads sinalizados.

  4. Se quiser se concentrar nos quatro threads sinalizados, você pode optar por mostrar apenas os threads sinalizados. Ele limita o que você vê nas janelas Threads da GPU, Inspeção Paralela e Pilhas Paralelas.

    Escolha o botão Mostrar Somente Sinalizados em qualquer uma das janelas ou na barra de ferramentas Depurar Local. A ilustração a seguir mostra o botão Mostrar Somente Sinalizados na barra de ferramentas Local de Depuração.

    Debug Location toolbar with Show Only Flagged icon.
    Botão Mostrar Somente Sinalizados

    Agora, as janelas Threads da GPU, Inspeção Paralela e Pilhas Paralelas exibem apenas os threads sinalizados.

Congelando e descongelando threads da GPU

Você pode congelar (suspender) e descongelar (retomar) threads da GPU da janela Threads da GPU ou da janela Inspeção Paralela. Você pode congelar e descongelar threads da CPU da mesma maneira. Para obter informações, consulte Como usar a janela Threads.

Para congelar e descongelar threads da GPU

  1. Escolha o botão Mostrar Somente Sinalizados para exibir todos os threads.

  2. Na barra de menus, escolha Depurar>Continuar.

  3. Abra o menu de atalho e, em seguida, escolha Congelar.

    A ilustração a seguir da janela Threads da GPU mostra que todos os quatro threads estão congelados.

    GPU Threads windows showing frozen threads.
    Threads congelados na janela Threads da GPU

    Da mesma forma, a janela Inspeção Paralela mostra que todos os quatro threads estão congelados.

  4. Na barra de menus, escolha Depurar>Continuar para permitir que os próximos quatro threads da GPU passem pela barreira na linha 22 e cheguem ao ponto de interrupção na linha 30. A janela Threads da GPU mostra que os quatro threads congelados anteriormente permanecem congelados e no estado ativo.

  5. Na barra de menus, escolha Depurar, Continuar.

  6. Na janela Inspeção Paralela, você também pode descongelar threads individuais ou de várias GPUs.

Para agrupar threads da GPU

  1. No menu de atalho de um dos threads na janela Threads da GPU, escolha Agrupar Por, Endereço.

    Os threads na janela Threads da GPU são agrupados por endereço. O endereço corresponde à instrução em desmontagem em que cada grupo de threads está localizado. 24 threads estão na linha 22 em que o método tile_barrier::wait é executado. 12 threads estão na instrução para a barreira na linha 32. Quatro desses threads estão sinalizados. Oito threads estão no ponto de interrupção na linha 30. Quatro desses threads estão congelados. A ilustração a seguir mostra os threads agrupados na janela Threads da GPU.

    GPU Threads window with threads grouped by Address.
    Threads agrupados na janela Threads da GPU

  2. Você também pode fazer a operação Agrupar por abrindo o menu de atalho para a grade de dados da janela Inspeção Paralela. Selecione Agrupar Por e escolha o item de menu que corresponde à forma como você quer agrupar os threads.

Executando todos os threads em um local específico no código

Você executa todos os threads em um determinado bloco para a linha que contém o cursor usando Executar Bloco Atual para Cursor.

Para executar todos os threads no local marcado pelo cursor

  1. No menu de atalho para os threads congelados, escolha Descongelar.

  2. No Editor de Código, coloque o cursor na linha 30.

  3. No menu de atalho do Editor de Código, escolha Executar Bloco Atual para Cursor.

    Os 24 threads que foram bloqueados anteriormente na barreira na linha 21 avançaram para a linha 32. Isso é mostrado na janela Threads da GPU.

Confira também

Visão geral do C++ AMP
Depurando código de GPU
Como usar a janela Threads da GPU
Como usar a janela Inspeção Paralela
Como analisar Código C++ AMP com o Visualizador de Simultaneidade