Estou tentando entender melhor o que seria necessário para um compilador ser capaz de fazer escolhas inteligentes em relação à simultaneidade em nome do programador. Percebo que há muitos aspectos difíceis desse problema, por exemplo:
- Garantir que não haja condições de corrida
Garantir que o código seja executado simultaneamente não terá efeitos colaterais que afetam o significado semântico do código
Decidindo se a sobrecarga de girar threads vale a pena, dado o grau de paralelismo disponível no código
Meu entendimento é que as duas principais representações intermediárias usadas em compiladores modernos são atribuição única estática para linguagens procedurais e orientadas a objetos e continuações passando estilo para linguagens funcionais. Raciocinar sobre qualquer um dos problemas listados acima parece difícil usando essas formas intermediárias. Mesmo as linguagens que, em teoria, deveriam ter a melhor chance de paralelização automática (linguagens funcionais puras como Haskell, com garantia de nenhum efeito colateral), fizeram progressos limitados nessa frente.
Então, minha pergunta é realmente que representações intermediárias foram usadas para tentar resolver esse problema? Existem outras representações que foram usadas na pesquisa acadêmica que eu não conheço e que são mais adequadas para essa tarefa? Esse é um problema que fundamentalmente precisa ser resolvido pelo front end do compilador, manipulando a árvore de sintaxe abstrata antes que a compilação alcance uma representação intermediária?
Respostas:
Supõe-se que a simultaneidade de modelagem explicitamente na representação intermediária (IR) seja um requisito necessário. Portanto, uma resposta seria "qualquer IR usado para programas seqüenciais com a adição de algumas operações de simultaneidade", por exemplo, "bifurcação e junção", "paralelo x y". A adição delas torna possível argumentar sobre alguns tipos de simultaneidade, mas não necessariamente facilmente. Também não é óbvio como garantir certas propriedades (liberdade de corrida de dados) sem chegar a uma representação totalmente funcional (o que dificulta a modelagem útil do paralelismo).
Indiscutivelmente Colorido Redes de Petri (CPNs) são uma boa escolha para representar programas com a concorrência. (Os tokens nos CPNs são "coloridos" [têm um tipo] e podem transportar valores; "transições" para estados podem executar aritmética arbitrária nos tokens recebidos para produzir um token possivelmente de cor diferente com valor calculado no "local"). Se você considerar os locais como resultados computados e transições como operadores de modelagem (incluindo um especial para acessar a memória), isso fornecerá o que equivale a um gráfico de fluxo de dados com o qual modelar programas. Você pode facilmente usar isso para fornecer uma interpretação formal às representações clássicas do compilador, como triplos [operador, entrada1, entrada2, saída].
Existem muitas ferramentas para analisar esses gráficos de CPN, incluindo propriedades de computação como ausência de impasse, limite de contagens de token em locais, etc. Os CPNs hierárquicos permitem modelar funções e procedimentos e a noção de "chamadas".
O que essas representações não fazem claramente é facilitar o raciocínio sobre onde alguém poderia paralelizar um aplicativo. Trivialmente, duas subcomputações podem ser paralelas se não tiverem efeito sobre operandos compartilhados (e é por isso que algumas pessoas adoram programas / representações funcionais). Se a sua representação de programa modelar uma memória compartilhada, você poderá modelá-la como monólito e obter o conjunto usual de problemas sobre o raciocínio sobre interações na memória compartilhada, incluindo endereçamento alternativo, etc. Uma maneira de evitar isso é tratar a memória como pedaços isolados com o estado maior do programa sendo alguns deles (semelhantes a árvores); você pode passar esses pedaços em sua representação intermediária. Não há interação entre dois cálculos paralelos se eles não compartilharem partes (por exemplo, subárvores de memória).
fonte