Processamento de fluxo - Stream processing

O processamento de fluxo é umparadigma de programação de computador , equivalente à programação de fluxo de dados , processamento de fluxo de eventos e programação reativa , que permite que alguns aplicativos explorem mais facilmente uma forma limitada de processamento paralelo . Tais aplicativos podem usar várias unidades computacionais, como a unidade de ponto flutuante em uma unidade de processamento gráfico ou matrizes de portas programáveis ​​em campo (FPGAs), sem gerenciar explicitamente a alocação, sincronização ou comunicação entre essas unidades.

O paradigma de processamento de fluxo simplifica o software e o hardware paralelos ao restringir a computação paralela que pode ser executada. Dada uma sequência de dados (um fluxo ), uma série de operações ( funções de kernel ) é aplicada a cada elemento do fluxo. As funções do kernel são normalmente canalizadas e tenta-se a reutilização da memória local no chip ideal, a fim de minimizar a perda de largura de banda, associada à interação da memória externa. O streaming uniforme , em que uma função do kernel é aplicada a todos os elementos do stream, é típico. Como o kernel e as abstrações de fluxo expõem dependências de dados, as ferramentas do compilador podem automatizar e otimizar totalmente as tarefas de gerenciamento no chip. O hardware de processamento de fluxo pode usar o placar , por exemplo, para iniciar um acesso direto à memória (DMA) quando as dependências se tornam conhecidas. A eliminação do gerenciamento manual de DMA reduz a complexidade do software, e uma eliminação associada para E / S em cache de hardware, reduz a extensão da área de dados que deve estar envolvida com o serviço por unidades computacionais especializadas, como unidades lógicas aritméticas .

Durante a década de 1980, o processamento de fluxo foi explorado na programação de fluxo de dados . Um exemplo é a linguagem SISAL (Streams e Iteration in a Single Assignment Language).

Formulários

O processamento de fluxo é essencialmente um compromisso, impulsionado por um modelo centrado em dados que funciona muito bem para DSP tradicionais ou aplicativos do tipo GPU (como processamento de imagem, vídeo e sinal digital ), mas nem tanto para processamento de propósito geral com acesso a dados mais aleatório ( como bancos de dados). Ao sacrificar alguma flexibilidade no modelo, as implicações permitem uma execução mais fácil, rápida e eficiente. Dependendo do contexto, o design do processador pode ser ajustado para eficiência máxima ou uma compensação para flexibilidade.

O processamento de fluxo é especialmente adequado para aplicativos que apresentam três características de aplicativo:

  • Compute Intensity , o número de operações aritméticas por E / S ou referência de memória global. Em muitas aplicações de processamento de sinal hoje, é bem acima de 50: 1 e aumenta com a complexidade algorítmica.
  • O paralelismo de dados existe em um kernel se a mesma função for aplicada a todos os registros de um fluxo de entrada e vários registros puderem ser processados ​​simultaneamente sem esperar pelos resultados dos registros anteriores.
  • Localidade de dados é um tipo específico de localidade temporal comum em aplicativos de processamento de sinal e mídia onde os dados são produzidos uma vez, lidos uma ou duas vezes mais tarde no aplicativo e nunca mais lidos. Os fluxos intermediários passados ​​entre os kernels, bem como os dados intermediários nas funções do kernel, podem capturar essa localidade diretamente usando o modelo de programação de processamento de fluxo.

Exemplos de registros dentro de fluxos incluem:

  • Em gráficos, cada registro pode ser o vértice, a normal e as informações de cor de um triângulo;
  • No processamento de imagem, cada registro pode ser um único pixel de uma imagem;
  • Em um codificador de vídeo, cada registro pode ter 256 pixels formando um macrobloco de dados; ou
  • No processamento de sinal sem fio, cada registro pode ser uma sequência de amostras recebidas de uma antena.

Para cada registro, podemos apenas ler a entrada, executar operações nele e gravar na saída. É permitido ter várias entradas e várias saídas, mas nunca um pedaço de memória que seja legível e gravável.

Comparação com paradigmas paralelos anteriores

Os computadores básicos começaram a partir de um paradigma de execução sequencial. CPUs tradicionais são baseadas em SISD , o que significa que conceitualmente executam apenas uma operação por vez. Conforme as necessidades de computação do mundo evoluíram, a quantidade de dados a serem gerenciados aumentou muito rapidamente. Era óbvio que o modelo de programação sequencial não poderia lidar com o aumento da necessidade de poder de processamento. Vários esforços foram feitos para encontrar maneiras alternativas de realizar grandes quantidades de cálculos, mas a única solução era explorar algum nível de execução paralela. O resultado desses esforços foi o SIMD , um paradigma de programação que permitia aplicar uma instrução a múltiplas instâncias de (diferentes) dados. Na maioria das vezes, o SIMD estava sendo usado em um ambiente SWAR . Usando estruturas mais complicadas, pode-se também ter paralelismo MIMD .

Embora esses dois paradigmas fossem eficientes, as implementações do mundo real eram afetadas por limitações, desde problemas de alinhamento de memória a problemas de sincronização e paralelismo limitado. Apenas alguns processadores SIMD sobreviveram como componentes independentes; a maioria estava embutida em CPUs padrão.

Considere um programa simples somando duas matrizes contendo 100 vetores de 4 componentes (ou seja, 400 números no total).

Paradigma convencional, sequencial

for (int i = 0; i < 400; i++)
    result[i] = source0[i] + source1[i];

Este é o paradigma sequencial mais familiar. Variações existem (como loops internos, estruturas e outros), mas elas acabam se resumindo a essa construção.

Paradigma SIMD paralelo, registradores compactados (SWAR)

for (int el = 0; el < 100; el++) // for each vector
    vector_sum(result[el], source0[el], source1[el]);

Na verdade, isso é simplificado demais. Ele assume que a instrução vector_sumfunciona. Embora seja isso o que acontece com os intrínsecos da instrução , muitas informações não são consideradas aqui, como o número de componentes do vetor e seu formato de dados. Isso é feito para maior clareza.

No entanto, você pode ver que esse método reduz o número de instruções decodificadas de numElements * componentsPerElement para numElements . O número de instruções de salto também é reduzido, pois o loop é executado menos vezes. Esses ganhos resultam da execução paralela das quatro operações matemáticas.

O que aconteceu, entretanto, é que o registro SIMD compactado contém uma certa quantidade de dados, portanto, não é possível obter mais paralelismo. A velocidade é um pouco limitada pela suposição que fizemos de executar quatro operações paralelas (observe que isso é comum para AltiVec e SSE ).

Paradigma de fluxo paralelo (SIMD / MIMD)

// This is a fictional language for demonstration purposes.
elements = array streamElement([number, number])[100]
kernel = instance streamKernel("@arg0[@iter]")
result = kernel.invoke(elements)

Neste paradigma, todo o conjunto de dados é definido, em vez de cada bloco de componente ser definido separadamente. A descrição do conjunto de dados é considerada nas duas primeiras linhas. Depois disso, o resultado é inferido das fontes e do kernel. Para simplificar, há um mapeamento 1: 1 entre os dados de entrada e saída, mas não precisa ser assim. Os grãos aplicados também podem ser muito mais complexos.

Uma implementação desse paradigma pode "desenrolar" um loop internamente. Isso permite que o rendimento seja dimensionado com a complexidade do chip, utilizando facilmente centenas de ALUs. A eliminação de padrões de dados complexos disponibiliza muito desse poder extra.

Embora o processamento de fluxo seja uma ramificação do processamento SIMD / MIMD, eles não devem ser confundidos. Embora as implementações SIMD possam muitas vezes funcionar de maneira "streaming", seu desempenho não é comparável: o modelo prevê um padrão de uso muito diferente que permite um desempenho muito maior por si só.

Foi observado que, quando aplicado em processadores genéricos, como CPU padrão, apenas um aumento de velocidade de 1,5x pode ser alcançado. Por outro lado, os processadores de fluxo ad-hoc atingem facilmente mais de 10x o desempenho, principalmente atribuído ao acesso mais eficiente à memória e a níveis mais altos de processamento paralelo.

Embora haja vários graus de flexibilidade permitidos pelo modelo, os processadores de fluxo geralmente impõem algumas limitações no kernel ou no tamanho do fluxo. Por exemplo, o hardware do consumidor frequentemente carece da capacidade de realizar matemática de alta precisão, carece de cadeias de indireção complexas ou apresenta limites inferiores no número de instruções que podem ser executadas.

Pesquisar

Os projetos de processamento de stream da Stanford University incluíram o Stanford Real-Time Programmable Shading Project, iniciado em 1999. Um protótipo chamado Imagine foi desenvolvido em 2002. Um projeto chamado Merrimac funcionou até cerca de 2004. A AT&T também pesquisou processadores aprimorados de stream à medida que as unidades de processamento gráfico evoluíram rapidamente. velocidade e funcionalidade. Desde esses primeiros dias, dezenas de linguagens de processamento de fluxo foram desenvolvidas, bem como hardware especializado.

Notas do modelo de programação

O desafio mais imediato no domínio do processamento paralelo não está tanto no tipo de arquitetura de hardware usada, mas em como será fácil programar o sistema em questão em um ambiente do mundo real com desempenho aceitável. Máquinas como a Imagine usam um modelo simples de thread único com dependências automatizadas, alocação de memória e programação DMA . Isso por si só é resultado da pesquisa no MIT e Stanford em encontrar uma camada ideal de tarefas entre programador, ferramentas e hardware. Os programadores superam as ferramentas no mapeamento de algoritmos para hardware paralelo e as ferramentas superam os programadores na descoberta de esquemas de alocação de memória mais inteligentes, etc. De preocupação particular são os designs MIMD, como Cell , para os quais o programador precisa lidar com o particionamento de aplicativos em vários núcleos e lidar com sincronização de processos e balanceamento de carga. Ferramentas de programação multi-core eficientes estão em falta hoje.

Uma desvantagem da programação SIMD era a questão de Array-of-Structures (AoS) e Structure-of-Arrays (SoA) . Os programadores muitas vezes queriam construir estruturas de dados com um significado 'real', por exemplo:

 // A particle in a three-dimensional space.
struct particle_t {
    float x, y, z;          // not even an array!
    unsigned byte color[3]; // 8 bit per channel, say we care about RGB only
    float size;
    // ... and many other attributes may follow...
};

O que aconteceu é que essas estruturas foram então montadas em matrizes para manter as coisas bem organizadas. Esta é uma matriz de estruturas (AoS). Quando a estrutura é disposta na memória, o compilador produzirá dados intercalados, no sentido de que todas as estruturas serão contíguas, mas haverá um deslocamento constante entre, digamos, o atributo "tamanho" de uma instância de estrutura e o mesmo elemento da seguinte instância. O deslocamento depende da definição da estrutura (e possivelmente de outras coisas não consideradas aqui, como as políticas do compilador). Existem também outros problemas. Por exemplo, as três variáveis ​​de posição não podem ser SIMD-izadas dessa forma, porque não há certeza de que serão alocadas no espaço de memória contínua. Para garantir que as operações SIMD possam funcionar neles, eles devem ser agrupados em um 'local de memória compactada' ou, pelo menos, em uma matriz. Outro problema reside na definição de "cor" e "xyz" em quantidades vetoriais de três componentes. Os processadores SIMD geralmente oferecem suporte para operações de 4 componentes apenas (com algumas exceções, no entanto).

Esses tipos de problemas e limitações tornaram a aceleração SIMD em CPUs padrão bastante desagradável. A solução proposta, estrutura de matrizes (SoA) segue como:

struct particle_t {
    float *x, *y, *z;
    unsigned byte *colorRed, *colorBlue, *colorGreen;
    float *size;
};

Para leitores sem experiência com C , o '*' antes de cada identificador significa um ponteiro. Nesse caso, eles serão usados ​​para apontar para o primeiro elemento de uma matriz, que será alocado posteriormente. Para programadores Java , isso é aproximadamente equivalente a "[]". A desvantagem aqui é que os vários atributos podem ser espalhados na memória. Para garantir que isso não cause falhas de cache, teremos que atualizar todos os vários "vermelhos", depois todos os "verdes" e "azuis".

Para processadores de fluxo, o uso de estruturas é encorajado. Do ponto de vista da aplicação, todos os atributos podem ser definidos com alguma flexibilidade. Tomando como referência as GPUs, existe um conjunto de atributos (pelo menos 16) disponíveis. Para cada atributo, o aplicativo pode indicar o número de componentes e o formato dos componentes (mas apenas os tipos de dados primitivos são suportados por enquanto). Os vários atributos são então anexados a um bloco de memória, possivelmente definindo uma distância entre elementos 'consecutivos' dos mesmos atributos, permitindo efetivamente os dados intercalados. Quando a GPU começa o processamento do fluxo, ela reúne todos os vários atributos em um único conjunto de parâmetros (geralmente isso se parece com uma estrutura ou uma "variável global mágica"), executa as operações e espalha os resultados para alguma área da memória para posterior processamento (ou recuperação).

Estruturas de processamento de fluxo mais modernas fornecem uma interface semelhante a FIFO para estruturar dados como um fluxo literal. Essa abstração fornece um meio de especificar as dependências de dados implicitamente enquanto permite que o tempo de execução / hardware aproveite ao máximo esse conhecimento para uma computação eficiente. Uma das modalidades de processamento de fluxo mais simples e eficientes até hoje para C ++ é o RaftLib , que permite vincular kernels de computação independentes como um gráfico de fluxo de dados usando operadores de fluxo C ++. Como um exemplo:

#include <raft>
#include <raftio>
#include <cstdlib>
#include <string>

class hi : public raft::kernel
{
public:
    hi() : raft::kernel()
    {
       output.addPort< std::string >( "0" ); 
    }

    virtual raft::kstatus run()
    {
        output[ "0" ].push( std::string( "Hello World\n" ) );
        return( raft::stop ); 
    }
};

int
main( int argc, char **argv )
{
    /** instantiate print kernel **/
    raft::print< std::string > p;
    /** instantiate hello world kernel **/
    hi hello;
    /** make a map object **/
    raft::map m;
    /** add kernels to map, both hello and p are executed concurrently **/
    m += hello >> p;
    /** execute the map **/
    m.exe();
    return( EXIT_SUCCESS );
}

Modelos de computação para processamento de fluxo

Além de especificar aplicativos de streaming em linguagens de alto nível, os modelos de computação (MoCs) também têm sido amplamente usados ​​como modelos de fluxo de dados e modelos baseados em processos.

Arquitetura de processador genérico

Historicamente, as CPUs começaram a implementar várias camadas de otimizações de acesso à memória devido ao desempenho cada vez maior quando comparado ao crescimento relativamente lento da largura de banda da memória externa. À medida que essa lacuna aumentava, grandes quantidades de área de dados foram dedicadas a ocultar latências de memória. Visto que buscar informações e códigos de operação para essas poucas ALUs é caro, muito pouca área de dados é dedicada ao maquinário matemático real (como uma estimativa grosseira, considere ser menos de 10%).

Uma arquitetura semelhante existe em processadores stream, mas graças ao novo modelo de programação, a quantidade de transistores dedicados ao gerenciamento é realmente muito pequena.

Começando do ponto de vista de todo o sistema, os processadores de fluxo geralmente existem em um ambiente controlado. As GPUs existem em uma placa adicional (isso parece se aplicar também ao Imagine ). CPUs fazem o trabalho sujo de gerenciamento de recursos do sistema, execução de aplicativos e assim por diante.

O processador de fluxo é geralmente equipado com um barramento de memória proprietário rápido e eficiente (os interruptores de barra cruzada agora são comuns, vários barramentos foram empregados no passado). A quantidade exata de faixas de memória depende da faixa de mercado. Enquanto isso está escrito, ainda existem interconexões de 64 bits (nível de entrada). A maioria dos modelos de médio porte usa uma matriz de switch crossbar rápida de 128 bits (4 ou 2 segmentos), enquanto os modelos high-end implantam grandes quantidades de memória (na verdade, até 512 MB) com uma barra transversal um pouco mais lenta com 256 bits de largura. Em contraste, os processadores padrão do Intel Pentium a alguns Athlon 64 têm apenas um único barramento de dados de 64 bits.

Os padrões de acesso à memória são muito mais previsíveis. Embora os arrays existam, sua dimensão é fixada na invocação do kernel. A coisa que mais se aproxima de uma indireção de ponteiro múltiplo é uma cadeia de indireção , que no entanto é garantida para finalmente ler ou escrever de uma área de memória específica (dentro de um fluxo).

Devido à natureza SIMD das unidades de execução do processador de fluxo (clusters ALUs), espera-se que as operações de leitura / gravação aconteçam em massa, então as memórias são otimizadas para alta largura de banda em vez de baixa latência (esta é uma diferença de Rambus e DDR SDRAM , para exemplo). Isso também permite negociações eficientes de barramento de memória.

A maior parte (90%) do trabalho do processador de fluxo é feita no chip, exigindo que apenas 1% dos dados globais sejam armazenados na memória. É aqui que vale a pena conhecer os temporários e dependências do kernel.

Internamente, um processador de stream apresenta alguns circuitos inteligentes de comunicação e gerenciamento, mas o que é interessante é o Stream Register File (SRF). Conceitualmente, trata-se de um grande cache no qual os dados do fluxo são armazenados para serem transferidos para a memória externa em massa. Como uma estrutura controlada por software semelhante a cache para as várias ALUs , o SRF é compartilhado entre todos os vários clusters de ALU. O conceito-chave e a inovação aqui feitos com o chip Imagine de Stanford é que o compilador é capaz de automatizar e alocar memória de uma maneira ótima, totalmente transparente para o programador. As dependências entre as funções do kernel e os dados são conhecidas por meio do modelo de programação que permite ao compilador realizar a análise de fluxo e empacotar os SRFs de maneira ideal. Normalmente, esse gerenciamento de cache e DMA pode ocupar a maior parte do cronograma de um projeto, algo que o processador de fluxo (ou pelo menos o Imagine) automatiza totalmente. Testes feitos em Stanford mostraram que o compilador fez um trabalho tão bom ou melhor na programação de memória do que se você ajustasse a coisa manualmente com muito esforço.

Existem provas; pode haver muitos clusters porque a comunicação entre clusters é considerada rara. No entanto, internamente, cada cluster pode explorar com eficiência uma quantidade muito menor de ALUs porque a comunicação dentro do cluster é comum e, portanto, precisa ser altamente eficiente.

Para manter essas ALUs buscadas com dados, cada ALU é equipada com arquivos de registro local (LRFs), que são basicamente seus registros utilizáveis.

Esse padrão de acesso a dados em três camadas torna mais fácil manter os dados temporários longe das memórias lentas, tornando a implementação de silício altamente eficiente e com economia de energia.

Problemas de hardware-in-the-loop

Embora um aumento da ordem de magnitude possa ser razoavelmente esperado (mesmo de GPUs convencionais ao computar de maneira streaming), nem todos os aplicativos se beneficiam disso. Latências de comunicação são, na verdade, o maior problema. Embora o PCI Express tenha melhorado isso com comunicações full-duplex, fazer com que uma GPU (e possivelmente um processador de fluxo genérico) funcione possivelmente levará muito tempo. Isso significa que geralmente é contraproducente usá-los para pequenos conjuntos de dados. Como alterar o kernel é uma operação bastante cara, a arquitetura de fluxo também incorre em penalidades para pequenos fluxos, um comportamento conhecido como efeito de fluxo curto .

O pipelining é uma prática muito difundida e amplamente utilizada em processadores stream, com GPUs apresentando pipelines que excedem 200 estágios. O custo para alternar as configurações depende da configuração que está sendo modificada, mas agora é considerado sempre caro. Para evitar esses problemas em vários níveis do pipeline, muitas técnicas foram implantadas, como "über shaders" e "atlas de textura". Essas técnicas são orientadas para jogos por causa da natureza das GPUs, mas os conceitos também são interessantes para processamento de fluxo genérico.

Exemplos

  • O Blitter no Commodore Amiga é um processador gráfico antigo (por volta de 1985) capaz de combinar três fluxos de origem de vetores de bits de 16 componentes de 256 maneiras para produzir um fluxo de saída consistindo de vetores de bits de 16 componentes. A largura de banda total do fluxo de entrada é de até 42 milhões de bits por segundo. A largura de banda do fluxo de saída é de até 28 milhões de bits por segundo.
  • Imagine, liderado pelo Professor William Dally, da Universidade de Stanford , é uma arquitetura flexível destinada a ser rápida e eficiente em termos de energia. O projeto, originalmente concebido em 1996, incluía arquitetura, ferramentas de software, uma implementação de VLSI e uma placa de desenvolvimento, foi financiado pela DARPA , Intel e Texas Instruments .
  • Outro projeto de Stanford , chamado Merrimac, visa desenvolver um supercomputador baseado em stream. Merrimac pretende usar uma arquitetura de fluxo e redes de interconexão avançadas para fornecer mais desempenho por custo unitário do que computadores científicos baseados em cluster construídos com a mesma tecnologia.
  • A família Storm-1 da Stream Processors, Inc , um spin-off comercial do projeto Imagine de Stanford , foi anunciada durante uma apresentação no ISSCC 2007. A família contém quatro membros que variam de 30 GOPS a 220 GOPS de 16 bits (bilhões de operações por segundo), todos fabricados na TSMC em um processo de 130 nanômetros. Os dispositivos são direcionados ao segmento de ponta do mercado de DSP , incluindo videoconferência , impressoras multifuncionais e equipamentos de vigilância por vídeo digital .
  • As GPUs são processadores de fluxo de nível de consumidor amplamente difundidos, projetados principalmente pela AMD e Nvidia . Várias gerações a serem observadas do ponto de vista de processamento de fluxo:
    • Pré-R2xx / NV2x: sem suporte explícito para processamento de fluxo. As operações do kernel ficavam ocultas na API e forneciam muito pouca flexibilidade para uso geral.
    • R2xx / NV2x: as operações de fluxo do kernel tornaram-se explicitamente sob o controle do programador, mas apenas para processamento de vértice (fragmentos ainda estavam usando paradigmas antigos). Nenhum suporte de ramificação prejudicou severamente a flexibilidade, mas alguns tipos de algoritmos podem ser executados (notadamente, simulação de fluido de baixa precisão).
    • R3xx / NV4x: suporte flexível de ramificação, embora ainda existam algumas limitações no número de operações a serem executadas e profundidade de recursão estrita, bem como manipulação de array.
    • R8xx: Suporta adicionar / consumir buffers e operações atômicas. Esta geração é o estado da arte.
  • Marca AMD FireStream para linha de produtos voltada para HPC
  • Marca Nvidia Tesla para linha de produtos voltada para HPC
  • O processador Cell da STI , uma aliança da Sony Computer Entertainment , Toshiba Corporation e IBM , é uma arquitetura de hardware que pode funcionar como um processador de fluxo com suporte de software apropriado. Consiste em um processador de controle, o PPE (Power Processing Element, um IBM PowerPC ) e um conjunto de coprocessadores SIMD, chamados SPEs (Synergistic Processing Elements), cada um com contadores de programa independentes e memória de instrução, na verdade uma máquina MIMD . No modelo de programação nativo, todo DMA e agendamento de programa são deixados para o programador. O hardware fornece um barramento de anel rápido entre os processadores para comunicação local. Como a memória local para instruções e dados é limitada, os únicos programas que podem explorar essa arquitetura com eficácia exigem uma pequena área de cobertura de memória ou aderem a um modelo de programação de fluxo. Com um algoritmo adequado, o desempenho do Cell pode rivalizar com o de processadores de fluxo puro; no entanto, isso quase sempre requer um redesenho completo de algoritmos e software.

Bibliotecas e linguagens de programação de streaming

A maioria das linguagens de programação para processadores de fluxo começa com Java, C ou C ++ e adiciona extensões que fornecem instruções específicas para permitir que os desenvolvedores de aplicativos identifiquem os kernels e / ou fluxos. Isso também se aplica à maioria das linguagens de sombreamento , que podem ser consideradas linguagens de programação de fluxo até um certo grau.

Exemplos não comerciais de linguagens de programação de fluxo incluem:

As implementações comerciais são de uso geral ou vinculadas a hardware específico de um fornecedor. Exemplos de linguagens de uso geral incluem:

Os idiomas específicos do fornecedor incluem:

Processamento baseado em eventos

Processamento baseado em arquivo em lote (emula parte do processamento de fluxo real, mas desempenho muito inferior em geral)

Processamento contínuo do fluxo do operador

Serviços de processamento de fluxo:

Veja também

Referências

links externos

  1. ^ Chintapalli, Sanket; Dagit, Derek; Evans, Bobby; Farivar, Reza; Graves, Thomas; Holderbaugh, Mark; Liu, Zhuo; Nusbaum, Kyle; Patil, Kishorkumar; Peng, Boyang Jerry; Poulosky, Paul (maio de 2016). "Benchmarking Streaming Computation Engines: Storm, Flink e Spark Streaming". 2016 IEEE International Parallel and Distributed Processing Symposium Workshops (IPDPSW) . IEEE. pp. 1789–1792. doi : 10.1109 / IPDPSW.2016.138 . ISBN 978-1-5090-3682-0. S2CID  2180634 .