Este artigo foi traduzido por máquina.

Windows com C++

O Visual C++ 2010 E A biblioteca de padrões Parallel

Kenny Kerr

Esta coluna se baseia em uma versão de pré-lançamento do Visual Studio 2010. Todas as informações estão sujeitas a alterações.

Conteúdo

Aprimoramentos de idioma
Algoritmos paralelos
Tarefas e grupos de tarefas

O Visual C++ é obter uma atualização importante na versão 2010 do Visual Studio. Muitos dos recursos novos idioma e a biblioteca são projetados exclusivamente para torná-la mais fácil e mais natural para expressar o seu desejo no código. Mas como sempre foi o caso com C++, a combinação desses recursos é o que torna o C++ tal uma linguagem poderosa e expressivo.

Portanto, este mês vou apresentar algumas as inclusões para a linguagem C++ que Visual C++ adicionou como parte da próxima C ++ 0 x padrão. Em seguida, analisarei oBiblioteca paralela de padrões(PPL) que a Microsoft desenvolveu sobre e acima a C ++ 0 x padrão para introduzir o paralelismo aos seus aplicativos de forma que naturalmente complementa a biblioteca C++ padrão.

Aprimoramentos de idioma

No artigo pode de 2008"C++ Plus: aprimorar aplicativos com o Visual C++ 2008 Feature Pack do Windows para cima" Apresentei as adições à biblioteca C++ padrão que parte técnico 1 de relatório (TR1) que foi originalmente introduzido com o Visual C++ 2008 Feature Pack e agora está incluído com o Visual Studio 2008 SP1. Nesse artigo, demonstrei o suporte para objetos de função por meio da classe de modelo de função e a função do modelo de ligação. Ser capaz de tratar funções polymorphically resolvidos muitos dos problemas inadequados os desenvolvedores do C++ geralmente teve ao escrever ou usar algoritmos genéricos.

Como um recapitular, aqui está um exemplo de como você inicializar um objeto de função com o padrão mais algoritmo:

function<int (int, int)> f = plus<int>();
int result = f(4, 5);

Com a Ajuda da função bind, você pode transformar uma função que fornece a funcionalidade necessária mas bastante não possui a assinatura à direita.

No exemplo a seguir, ESTOU usando a função de ligação com os espaços reservados para inicializar o objeto de função com uma função de membro:

struct Adder
{
   int Add(int x, int y, void* /*reserved*/)
   {
       return x + y;
   }
};

Adder adder;
function<int (int, int)> f = bind(&Adder::Add, &adder, _1, _2, 
    static_cast<void*>(0));
int result = f(4, 5);

Há dois problemas que surgem com o uso dessas adições de biblioteca não pode ser facilmente superar sem aprimoramentos de idioma. Para iniciantes, é geralmente ineficiente para definir explicitamente um objeto de função, como ela adiciona determinada sobrecarga que o compilador caso contrário, poderia ter evitados. Ele também pode ser bastante redundante e entediante a re-declare o protótipo de função quando o compilador sabe claramente a assinatura que melhor coincide com a expressão de inicialização.

Isso é onde a palavra-chave automática novo ajudará a. Ele pode ser usado no lugar de explicitamente definir o tipo de uma variável, que é útil no modelo metaprogramming em que os tipos específicos estão difícil definir ou complicada expressar. Eis o que ele se parece com:

auto f = plus<int>();

As definições das funções próprias também podem se beneficiar do melhora. Geralmente você pode reutilizar algoritmos úteis, como aquelas em que a biblioteca C++ padrão. Mais freqüentemente que não, no entanto, você precisará escrever algumas funções específicas de domínio para uma finalidade bem específica que não é geralmente reutilizável.

Mas porque a função deve ser definida em outro lugar, você deve considerar design lógico e físico. Não seria ótimo a definição pode seja colocado no local exatamente onde ele for necessário, simplificando o entendimento do código, melhorando a localidade da lógica e melhorando o encapsulamento do design geral? A adição de expressões lambda permite exatamente que:

auto f = [](int x, int y) { return x + y; };

As expressões lambda definem objetos de função sem nome, às vezes chamados de feriados. O [] é a dica que informa o compilador que uma expressão lambda está começando. Isso é conhecido como o introducer lambda, e ele é seguido de uma lista de parâmetros. Esta declaração de parâmetro também pode incluir um tipo de retorno, embora geralmente for omitido quando o compilador pode deduzir o tipo sem ambigüidade, como é o caso no trecho de código anterior. Na verdade, a própria lista de parâmetro pode ser omitida se a função é aceitar sem parâmetros. A expressão lambda termina com qualquer número de instruções de C++ dentro de chaves.

As expressões lambda também podem capturar variáveis para uso dentro a função do escopo no qual a expressão lambda é definida. Aqui está um exemplo que usa uma expressão lambda para calcular a soma dos valores em um contêiner:

int sum = 0;
for_each(values.begin(), values.end(), [&sum](int value)
{
    sum += value;
});

Concedido, isso poderia foram executado mais sucintamente possível com a função accumulate, mas que pode estar faltando o ponto. Este exemplo demonstra como a variável de soma é capturada pelo referência e usada dentro da função.

Algoritmos paralelos

O PPL apresenta um conjunto de construções de paralelismo orientados a tarefas, bem como um número dos algoritmos paralelos semelhante ao que está disponível com OpenMP hoje. Os algoritmos PPL, no entanto, escritos com modelos C++ em vez de diretivas pragma e, como resultado, são muito mais expressivo e flexível. O PPL Entretanto, é fundamentalmente diferente das OpenMP no sentido de que o PPL promove um conjunto de primitivos e algoritmos que são mais compostas e reutilizáveis como um conjunto de padrões. Enquanto isso, OpenMP é inerentemente mais declarativa e explícita em assuntos como o agendamento e, em última análise, não é parte do C++ adequado. O PPL também está embutido no parte superior do Runtime do concorrente, permitindo maior interoperabilidade potencial com outras bibliotecas com base no tempo de execução mesmo. Vamos examinar os algoritmos PPL e, em seguida, veja como você pode usar a funcionalidade subjacente diretamente para paralelismo em orientados a tarefas.

Na edição de outubro de 2008 desta coluna " Explorando os algoritmos de alto desempenho"), Demonstrei o benefício dos algoritmos eficientes e os efeitos da localidade e designs de cache-sensível no desempenho. Mostrei como um algoritmo ineficiente, single-threaded para converter uma imagem grande em escala de cinza demorou 46 segundos, enquanto uma implementação eficiente ainda apenas usando um único segmento levou apenas 2 segundos. Com um pouco sprinkling do OpenMP que foi capaz de colocar em paralelo o algoritmo sobre o eixo Y e reduzir o tempo ainda mais. a Figura 1 mostra o código para o algoritmo de OpenMP.

Algoritmo de escala de cinza a Figura 1 usando OpenMP

struct Pixel
{
    BYTE Blue;
    BYTE Green;
    BYTE Red;
    BYTE Alpha;
};

void MakeGrayscale(Pixel& pixel)
{
    const BYTE scale = static_cast<BYTE>(0.30 * pixel.Red +
                                         0.59 * pixel.Green +
                                         0.11 * pixel.Blue);

    pixel.Red = scale;
    pixel.Green = scale;
    pixel.Blue = scale;
}

void MakeGrayscale(BYTE* bitmap,
                   const int width,
                   const int height,
                   const int stride)
{
    #pragma omp parallel for
    for (int y = 0; y < height; ++y)
    {
        for (int x = 0; x < width; ++x)
        {
            const int offset = x * sizeof(Pixel) + y * stride;

            Pixel& pixel = *reinterpret_cast<Pixel*>(bitmap + offset);

            MakeGrayscale(pixel);
        }
    }
}

O PPL inclui um paralelo para algoritmos que muito naturalmente, pode com uma pequena ajuda de expressões lambda, substituem o uso de OpenMP na função MakeGrayscale de A Figura 1 :

parallel_for(0, height, [&] (int y)
{
    for (int x = 0; x < width; ++x)
    {
        // omitted for brevity
    }
});

Como você pode ver, o de loop bem como o pragma OpenMP foram substituído pela função parallel_for. Dois primeiros parâmetros a função definir o intervalo de iteração, assim como para o anterior para loop. Ao contrário do OpenMP, que impõe restrições pesadas ao para a diretiva, parallel_for é uma função do modelo para que você pode, por exemplo, iterar sobre tipos sem sinal ou os iteradores ainda complexos dos recipientes padrão. O último parâmetro é um objeto de função, o que eu defino como uma expressão lambda.

Você observará que o introducer lambda inclui somente um e comercial sem declarando explicitamente todas as variáveis para capturar. Isso informa o compilador para capturar todas as variáveis possíveis por referência. Como as declarações dentro a expressão lambda usam um número de variáveis, usei isso como uma abreviada. Tenha cuidado, no entanto, porque o compilador é não é possível otimizar imediatamente todas as não utilizadas variáveis, resultando em desempenho ruim em tempo de execução. Pode ter explicitamente capturei as variáveis que eu precisava com a seguinte lista de captura:

 [&bitmap, width, stride]

Assim como o parallel_for função é uma alternativa paralela para o loop fornecendo iteração paralela em um intervalo de índices, o PPL também fornece a função de modelo parallel_for_each como uma alternativa paralela para a função for_each padrão. Ela fornece iteração paralela em um intervalo de elementos definidos por um par de iteradores, tais como aqueles fornecido pelos recipientes padrão. Embora ele feita mais sentido para o exemplo anterior, use a função parallel_for com índices explícitas, geralmente é mais natural para usar os iteradores para definir um intervalo de elementos. Dada uma matriz de números, você poderia quadrado seus valores usando a função parallel_for da seguinte maneira:

array<int, 5> values = { 1, 2, 3, 4, 5 };

parallel_for(0U, values.size(), [&values] (size_t i)
{
    values[i] *= 2;
});

Mas essa abordagem é muito detalhada, requer a matriz para ser capturado pela expressão lambda e, dependendo do tipo de recipiente, pode ser ineficiente. A função parallel_for_each resolve esses problemas bem:

parallel_for_each(values.begin(), values.end(), [] (int& value)
{
    value *= 2;
});

Se você apenas deseja executar um número de funções em paralelo, ou como paralelo como é possível baseado no número de segmentos de hardware disponíveis, você pode usar a função de modelo parallel_invoke. Há sobrecargas disponíveis para aceitar em qualquer lugar de 2 a 10 objetos de função. Aqui está um exemplo de executar três funções lambda em paralelo:

combinable<int> sum;

parallel_invoke([&] { sum.local() += 1; },
                [&] { sum.local() += 2; },
                [&] { sum.local() += 3; });

int result = sum.combine([] (int left, int right)
{
    return left + right;
});

ASSERT(6 == result);

Este exemplo também ilustra outra classe auxiliar fornecida pelo PPL. A classe combinada torna incrivelmente fácil combinar os resultados de um número de tarefas paralelas com bloqueio mínimo. Fornecendo uma cópia local do valor para cada segmento e, em seguida, apenas combinando os resultados de cada segmento após o trabalho paralelo concluiu, a classe combinada evita muita o bloqueio que normalmente ocorre em tais casos.

Tarefas e grupos de tarefas

O paralelismo real nos algoritmos que discuti é obtido por meio de uma API orientados a tarefas simples que você está livre para usar diretamente. Tarefas são definidas com a classe task_handle inicializada com um objeto de função. As tarefas são agrupadas em conjunto com uma classe task_group que executa as tarefas e aguarda-los para concluir. Obviamente, o task_group fornece sobrecargas úteis para que em muitos casos você nem precisa que definir os objetos task_handle si mesmo e pode permitir que o objeto de task_group alocar e gerencie suas vidas úteis para você. Aqui está um exemplo de como usar um task_group para substituir a função parallel_invoke do exemplo anterior:

task_group group;
group.run([&] { sum.local() += 1; });
group.run([&] { sum.local() += 2; });
group.run([&] { sum.local() += 3; });
group.wait();

Além de algoritmos e APIs já discutidos aqui, outros algoritmos paralelos e classes auxiliares podem também ser incluídos quando a biblioteca paralela de padrões Finalmente é liberada com Visual C++ 2010. Para manter atualizado no mais recente de concorrência, visite Paralelo programação no código nativo.

Envie suas dúvidas e comentários para mmwincpp@Microsoft.com.

Kenny Kerr é um profissional de fabricação de software especializado no desenvolvimento de software para o Windows. Ele adora escrever e ensinar aos desenvolvedores sobre design de programação e software. Alcançar Kenny em weblogs.asp. NET/kennykerr.