Вы находитесь на странице: 1из 15

Apresentando a SSA

(Static Single Assignment)


Eduardo Spaki1
1
DIN – Departamento de Informática da Universidade Estadual de Maringá (UEM)
Maringá – PR – Brazil
spoky.br@gmail.com

Abstract. In this paper, it is explaining the SSA optimization technique, a op-


timization technique for compilers, which improves many others algorithms of
optimization. The explain shows some of its details (as the concept and the idea
of the technique), readings and criticals about the same, having as a base the
vision of some authors. It will be showed how to around the obscure situa-
tions, as points of decisions, with the φ-functions. Some algorithms to use the
SSA Form will be quickly explained, tracing a parallel among source code, flow
graph and dominance tree. In the paper it will be also improved importance of
the technique and its use.

Resumo. Neste artigo, é explicada a técnica de otimização SSA, uma técnica


de otimização para compiladores, a qual beneficia muitos outros algoritmos de
otimização. A explicação abordará alguns de seus detalhes (como o conceito e a
concepção da técnica), leituras e crı́ticas sobre a mesma, baseando-se na visão
de alguns autores. Será exposto como contornar as situações obscuras, como
pontos de tomadas de decisão, com as φ-functions. Alguns algoritmos para
a adoção da forma SSA serão brevemente explicados, trançando um paralelo
entre código fonte, fluxo de controle e arvore de dominância. No artigo será
ressaltada também a importância da técnica e a adoção da mesma.

1. Introdução
A SSA, ou Static Single Assignment, é uma das inúmeras técnicas de otimização que
podem ser aplicadas e implementadas em um compilador.
É uma técnica independente de linguagem e processadores (arquitetura), pois ela
é aplicada em uma representação intermediária da compilação. Logo após a aplicação
da SSA, e as otimizações que se beneficiam dela, é necessário ”desfazer” a SSA para o
código ser representado adequadamente no hardware.
Ela foi concebida na década de 80 nos laboratório de pesquisa da IBM, com a
finalidade de simplificar o fluxo de controle e tornar mais fácil a adoção de técnicas que
são beneficiadas pela declaração única de uma variável.
Antigamente algumas otimizações neste sentido eram feitas utilizando uma
técnica chamada def-use chain, que é uma eficiente estrutura de dados que contém o
campo de uso de cada variável e para cada declaração o compilador mantém os ponteiros
da variável, segundo Appel (1998), mantendo assim cada definição das variáveis aos usos
que cada uma alcança.
2. Definição
Neste conceito de otimização, cada variável, que pode ser chamada de ”pseudo-
registrador”, é atribuı́da apenas uma única vez, e cada uso da variável é dominado pela
definição dela. Isto viabiliza uma análise mais rápida do código, tornando mais eficiente
e fácil aplicar outras otimizações. Com a SSA é mais fácil criar um grafo de fluxo de
controle, ou controle de dependencia, e até mesmo uma análise de data-flow.
Com a simplificação realizada pelo formato SSA, apenas uma única definição
pode alcançar um uso particular da variável, assim, achar a tal definição é trivial. Ela
também permite algoritmos, insensı́veis ao fluxo, gozar dos benefı́cios de algoritmos
sensı́veis ao fluxo, sem o custo de análises de fluxo de dados.
Esta técnica foi originalmente desenvolvida por Ron Cytron, Jeanne Ferrante,
Barry Rosen, Mark Wegman, and Ken Zadeck. Eles eram pesquisadores da IBM na
década de 80.

3. Detalhes
Um simples exemplo de atribuições pode ilustrar o ganho que há na análise de um código
com SSA, como no algoritmo a seguir:

A = 1;
A = 2;
B = A;

Neste exemplo, a lógica é que a primeira linha, ou seja, a primeira atribuição é


dispensável. Assim, o valor que irá alimentar a variável ”B” provém da ”segunda missão”
da variável ”A”. Para um compilador identificar esta otimização, teria de criar um grafo
do fluxo da aplicação e analisá-lo. A SSA acaba individualizando as variáveis e seus usos.
Logo, a identificação do fluxo e as tarefas que serão otimizadas, tornam-se evidentes:

A1 = 1;
A2 = 2;
B1 = A2 ;

Neste caso, um algoritmo de ”eliminação de código morto” (detalhes sobre esta


tecnica serão melhores detalhados neste artigo) se beneficiaria ao identificar a ”inativi-
dade” de ”A1 ”.

4. Os Algoritmos
Mayers (2003, p. 3) define SSA como um conceito que explicita o fluxo de informações
de dados de um determinado programa, concentrando na maneira como é realizada a
definição (ou seja, atribuição) e uso de variáveis.
Briggs (p. 1) apresenta que a SSA é uma representação intermediária que os com-
piladores utilizam para facilitar a análise do aplicativo e beneficia as otimizações. Appel
(1998, p. 443) expõe que muitas análises de dataflow precisam encontrar as dominan-
cias de cada definição de variável, e que para isto existe o def-use chain. Mas este autor
apresenta a SSA como uma melhoria desta ideia, definindo-a como ”uma representação
intermediária na qual cada variável tem apenas uma definição no texto do programa”.
O autor Mayers (2003) ressalta que há complicações evidentes quando há fluxos
de controles (tomadas de decisões com if, laços com um for, etc.). A Figura 1 demonstra
o fluxo de controle do algoritmo proposto.

Figure 1. Fluxo de controle do algoritmo proposto

Logo, deve ocorrer a definição de uma estratégia para portar a aplicação para a
forma SSA.
Segundo o autor (MAYER, 2003), há dois passos fundamentais para a adoção da
SSA.
O primeiro passo fundamental que Mayers apresenta é uma declaração especial,
chamada de φ-function. Esta função, que será tratada com mais detalhes neste artigo,
basicamente cuidará dos nós propostos no algoritmo, representando as possibilidades, do
uso de uma variável numerada, perante uma bifurcação.
O segundo é a re-nomeação das variáveis de acordo com sua dominância.
Ambos os passos citados podem ser vistos na Figura 2.
Foi levantada então uma fase inicial, a identificação das necessidades das φ-
function, e suas aplicações. A Figura 3 demonstra outro algoritmo passando pelas três
fases.
Com isto, é preciso propor soluções práticas para que algoritmos sejam aplica-
dos aos códigos para portá-los para o SSA. Mayers (2003) apresenta algumas questões,
melhorando-as, além de propor dois algoritmos para as mesmas.
Mayers (2003), ressalta também que há métodos e técnicas especı́ficas para lidar
facilmente com arrays e records e Appel extende isto até os ponteiros.
Figure 2. Renomeação de variáveis

Figure 3. Fases da SSA

4.1. Tratando os Arrays

Uma atribuição de um array para outro, por exemplo, pode ser tratada com uma atribuição
entre variáveis escalares. No entanto, as atribuições dos arrays, muitas vezes tomam a
forma de atribuições de um elemento da matriz. Mas, mesmo neste caso, a idéia é usar a
matriz (array) como uma variável escalar. Para realizar as operações em arrays, Mayes
(2003) ilustra métodos como: Acesso (Access) e Atualizar (Update) - veja a Figura 4:

4.2. Tratando os Records

A fim de ajustar as estruturas records para o a forma SSA, podemos aplicar a mesma
técnica, citada para os arrays, considerando o registro como uma matriz e os campos de
registro como elementos da matriz.
Figure 4. Manipulação de Arrays

4.3. Ponteiros e dependência de memória


Appel (1998) expõe os controles citados por Mayers como Armazenar (store) e Carregar
(load). Esta forma acaba fazendo sentido, pois, para o autor, um programa real deve ler e
armazenar words no memória.
Ambas as ténicas são maneiras de acessar registros alocados dentro de uma
coleção, seja ela a memória ou uma matriz.
Para muitas propostas de otimização e escalonamento, o compilador precisa saber
”como a declaração B depende da declaração A?” Para a forma SSA nunca ocorrera uma
situação onde as declarações ”A” e ”B” escrevem na mesma variável. Então, com esta
dependência de memória, o compilador tem que assegurar que cada alocação de memoria
deve ser escrita uma única vez.
Para uma melhor compreensão, considere o código a seguir:

M[i] = 4;
x = M[j];
M[k] = j;

Não há como tratar cada posição da memória individualmente, como uma variável
separada, porque não ha como saber se i, j, e k é o mesmo endereço.
Mas é possı́vel tratar a memória como uma ”variável”, onde a instrução load cria
um novo valor (da memória inteira), acompanhe o racı́ocı́nio no código a seguir:

M1 = store(M0 , i, 4);
x = load(M1 , j);
M2 = store(M1 , k, j);

E mesmo se uma outra tecnica de otimização re-ordenar as declaraçõe, do código


proposto, o acesso a memória permanece correto. Acompanhe a lógica:

M1 = store(M0 , i, 4);
M2 = store(M1 , k, j);
x = load(M1 , j);

Appel (1998) propôs esta solução que acaba, como Mayers, cobrindo de maneira
semelhante, tendo a única alteração no nome dos métodos, ou funções, que tratam a
leitura e atribuição de valores em estruturas como matrizes e outras estruturas mantidas
em memória, como ponteiros.

4.4. Fronteira de Dominâncias


O primeiro algoritmo é a Fronteira de Dominâncias (Dominance Frontiers). A idéia é cal-
cular fronteiras de dominância e então usá-las para determinar onde colocar φ-functions.
O conceito de dominância pode ser compreendido em dizer que um nó ”B” domina
um nó diferente, no grafo de fluxo de controle, se for impossı́vel chegar em ”B” sem
passar por este outro nó, nem que seja uma única vez.
Isso é útil, porque se alguma vez chegar a ”B”, fica válida a idéia de que o código
da dominância de B foi executado.
Para definir as fronteiras de dominância, um nó ”B” estará na fronteira de
dominância de um nó ”A” se ”A” terminantemente não dominar ”B”, mas não dominam
alguns antecessores imediatos de ”B”.
As fronteiras de dominância capturam os locais exatos que devem ser colocadas
as φ-functions.
A definição de uma variável ”V” é dita um domı́nio da utilização de ”V”, se o uso
pode ser rastreado até uma definição única. No entanto, ao entrar nos fluxos de controle,
a variável apresentará várias definições, conseqüentemente novos domı́nios. São estas as
fronteiras dinâmicas (pois depende do fluxo que a aplicação tomar durante sua execução),
e é nestes locais que deve ser inserida a φ-function.
Veja na Figura 5 um exemplo de Fronteira de Dominância:

Figure 5. Fronteira de Dominância

Repare que não há como chegar nos nós circulados, sem antes passar pelo nó
denominado ”5”, os nós dentro da fronteira do nó ”5”, por sua vez, estabeleciriam suas
próprias fronteiras de dominância, evitando assim que o nó ”5” englobe todos os nós.
Para tal técninca evoluir dentro da SSA, é preciso determinar a árvore de
dominância, que pode ser construı́da com propriedades do fluxo de controle correspon-
dente.
Os nós da árvore e do fluxo de controle são idênticos, segundo Mayers (2003), e
as bordas da árvore são determinadas usando a propriedade de dominância imediata do
fluxo de controle como é demonstrado na Figura 6.

Figure 6. Árvore de Dominância

Compreendido o conceito da fronteira dinâmica, Mayers (2003) propõe algorit-


mos de Cytron que provaram que DF(X) pode ser expressa como uma união de dois
subconjuntos, DFlocal e DFup, e que ambos os subgrupos podem ser calculados com
testes simples, usando a igualdade de informações do fluxo de controle e da árvore de
dominância. Para a árvore proposta, seu estudo de computação prevê que ela poderia ser
computada da maneira representada na Figura 7.
Uma vez realizada a computação, seu resultado indicaria a inclusão das φ-
functions e a re-nomeação de variáveis.
A Figura 8 exemplifica um algoritmo proposto para computar o conjunto da Fron-
teira de Dominância. Este algoritmo foi originalmente proposto por Cytron, autor já
citados pelos autores nos quais este artigo se baseia.
Neste código um antecessor do nó ”n” é qualquer nó do qual o controle é trans-
ferido para o nó ”n”, e ”IDOM (n)” é o dominador imediato do nó ”n”.

4.5. Geração Simples de SSA


O segundo algoritmo proposto por Mayers (2003) provém de um estudo que ele fez em
materiais de 1997, dos autores Aycock e Horspool. Trata-se da Geração Simples de SSA
(Simple Generation of SSA Form).
Figure 7. Computação da árvore de dominância proposta

É um algoritmo de duas fases, que se baseia na simples idéia: primeiro devem ser
inseridas as φ-funcions quando possı́vel. Em seguida, iterativamente, excluir aquelas que
não são necessárias.
A primeira fase do algoritmo é chamada de RC, onde serão distribuı́das as funções
φ livremente. A segunda é a fase de minimização das funções φ, ou seja, é feita uma limpa
das funções, tentanto otimizar as chamadas da mesma, como pode ser visto na Figura 9.
O autor (Mayer, 2003) complementa que Aycock and Horspool propõe
otimizações neste algoritmo, como: One-pass RC phase, Mapping table. Basic blocks
with single predecessors.
Mayers (2003) conclui que os algoritmos para SSA propostos por Cytron, Ay-
cock e Horspool são eficientes e que o SSA se tornou uma técnica padrão, amplamente
utilizada, relembrando, inclusive, sua implementação no compilador GNU, o GCC.

5. Compreendendo melhor a φ-funcion


Quando há mais de um caminho que pode ser tomado em um código, devido a algum fluxo
de controle, como tomadas de decisão ou loops, fica obscuro determinar a declaração
única das variáveis. Por exemplo: em um if-then-else uma variável ”V” pode assumir
um valor no if e outro no else. A princı́pio não há como saber qual a definição que ela
Figure 8. Algoritmo proposto para computar as Fronteiras de Dominâncias

Figure 9. Minimização das φ-funcions

assumiu.
Para suprir esta necessidade é que foi introduzida a função φ. Segundo Appel
(1998, p. 434-435) ela expõe as variáveis resultantes do nó.
Appel (1998) propõe uma solução para conversão de um programa para a forma
SSA muito próxima a Geração Simples de SSA citada por Mayers (2003), na qual
primeiramente deve-se adicionar as φ-functions e, em seguida, re-nomear as variáveis.

6. Uma alternativa a φ-funcion

O autor Briggs propõe uma alternativa para as φ-funcions.Trata-se das ”Cópias” (Copies).
Briggs propõe isto baseando-se na idéia de que raramente algum computador tenha um
hardware capaz de representar e executar as φ-funcions.Com isto os compiladores já
acabam fazendo a cópia naturalmente. Porém é possı́vel refiná-la com a Coloração de
Grafos do Fluxo de controle.
Briggs ressalta que a Coloração de Grafos, não é uma tarefa trivial, mas se não for
executada adequadamente o compilador pode executar o ”copy folding” ingenuamente,
levando a erros.
Briggs monta o algoritmo da Figura 10 para escalonar o suo das Cópias, definidas
pelo mesmo.
7. O Uso e Aplicação da SSA
Como já mencionado no artigo, a forma SSA é utilizada para a otimização do programa
em um compilador. Segundo Mayers (2003) as seguintes otimizações podem se beneficiar
de SSA Form.

7.1. Propagação de Constante


A propagação de contante (Constant Propagatio) avalia uma constante e propaga esta
informação a todos os locais no código onde existe uma variável que a representaa. Assim
ocorreria a eliminação de variáveis, deixando seus valores explı́citos no código.
Veja um código apresentado por Mayers (2003) na Figura 11.

7.2. Eliminação de Redundância Parcial


Basicamente, a Eliminação de Redundância Parcial (Elimination of Partial Redundancies)
é a eliminação de cálculos que são realizados duas, ou mais, vezes com os mesmos valores
de entrada e saı́da.
Veja outro exemplo que o Mayers (2003) fornece na Figura 12.

7.3. Deslocamento de Código


O Deslocamento de Código (Code Motion) é uma outra técnica para evitar cálculos
desnecessários de valores em tempo de execução, principalmente dentro de laços de
repetição.
Além disso, introduz variáveis temporárias para armazenar valores calculados,
visando inicializá-los em locais adequados no código.
Uma estratégia, denominada de deslocamento de código seguro, é colocar estes
inicializações o mais cedo possı́vel. A Lazy, ao contrário, coloca o inicializações tão
tarde quanto possı́vel, visando a otimização computacional, ou seja, Lazy atrasaria ao
máximo a inicialização de um valor, até que o valor fosse realmente necessário para a
continuação do programa. Esta estratégia resulta em uma vida útil mı́nima das variáveis
temporárias.
Na Figura 13 é demonstrada a tradução do código para a forma SSA, tornando
possı́vel passar a atribuição a execução de algumas tarefas apenas uma única vez.

7.4. Eliminação de Código Morto


Eliminação de Código Morto (Dead Code Elimination) é a eliminação de códigos que são
computados mas não alteram nada relevante para o resultado e/ou ambiente. São códigos
ineficazes. Acompanhe na Figura 14.

8. Retornando da Forma SSA


Após a transformação e otimização do código, um programa na forma SSA deve ser
traduzido em uma representação executável, retirando as φ-funcions, segundo Appel
(1998).
A SSA acaba não sendo mais útil na execução direta, logo as funções de mapea-
mento, sobre as quais a SSA foi fundada, podem ser descartadas. Assim restará uma
representação intermediária otimizada. Além disto, como já citado, a φ-funcion geral-
mente não é reconhecida pelo hardware.
Existem várias formas de sair da forma SSA. Briggs, que expõe a limitação das
φ-funcions perante o hardware, cita o uso de ”Cópias” sobre a representação do grafo de
interferência.

9. Curiosidades
O compilador GCC, a partir da sua versão 4.0 (Julho de 2005) introduziu em suas
otimizações a SSA. Antes, a representação intermediária gerada por ele era chamada de
RTL (Register Transfer Language).

A RTL é uma representação de nı́vel baixo, muito próxima à linguagem


assembly (inspirada pelas expressões LISP S). O problema com a RTL é
que as otimizações que ela permite são muito próximas ao destino. As
otimizações que requerem informações de nı́vel maior sobre o programa
podem não ser possı́veis, pois suas expressões são perdidas na RTL. A
árvore SSA foi criada para ser independente de linguagem e de destino,
suportando ainda análises melhoradas e otimizações mais valiosas. (IBM,
Seção Conhecendo o GCC 4, 2008)

É importante ressaltar que, por ser independente de linguagens, a técnica não se


limita apenas às linguagens imperativas.
Alguns algoritmos de otimização são diretamente beneficiados pelo SSA, como:
constant propagation , sparse conditional constant propagation, dead code elimination,
global value numbering, partial redundancy elimination, strength reduction, register al-
location. Alguns destes descritos neste artigo.

10. Crı́tica
Os autores, já citados, em momento algum apresentaram um histórico mais preciso da
técnica SSA. A conseqüência é a falta de clareza na hora de ressaltar os reais benefı́cios
da técnica diretamente ligada ao produto final da compilação. Assim ficou apenas demon-
strado que a técnica beneficia outros algoritmos de otimização de código, pois a SSA
simplifica a analise de fluxo de controle do código, evidenciando o tempo de vida das
variáveis. Isto fez com que a SSA adquirisse uma classificação de técnica auxiliar.
A SSA é uma técnica que tem que ser aplicada, e spos os processamento de
código pertinentes, deve-se ”retirá-la de cena”. Isto acaba comprometendo o tempo de
compilação. Assim, algumas plataformas, que próvem uma recompilação de código, após
uma análise do comportamento da aplicação, para melhorar o desempenho de algumas
rotinas, acabam não se beneficiando tanto desta técninca, pois o tempo de compilação
acaba embutido no tempo de execução .
Os autores poderiam ter deixado claro se a SSA sozinho traz algum benefı́cio ao
produto final do compilador.
Além disto, pela falta de precisão do histórico, não ficou claro qual foi o real
motivo que impulsionou os pesquisadores da IBM a desenvolver algo desta natureza.
Os autores também ficaram devendo mais exemplos da evolução de um código
normal para um otimizado, passando pela SSA. Além disto poderiam ter traçado exemplos
colocando lado a lado a SSA e o use-def Chain.

11. Estudos Futuros


Estudar a aplicação da SSA em Linguagens que rodam sobre um ambiente de execução
próprio, como JAVA com a JVM (Java Virtual Machine) e C# com a CLI (Common Lan-
guage Infrastructure). Assim será possı́vel estudar em um nı́vel mais alto a SSA, além de
verificar os ganhos de análise de código para otimização da compilação, pois em muitos
casos, estes ambientes acabam embutindo a compilação no tempo de execução (JIT - Just
in Time), criando assim, uma oportunidade de realizar algumas avaliações empı́ricas.
Traçar um paralelo entre a SSA aplicada a linguagens compiladas (C, C++, Pascal
etc.) e linguagens compiladas em tempo de execução de acordo com seu ambiente de
execução (JAVA, C# etc.). O objetivo é identificar onde há maior ganho, além de veri-
ficar se há uma necessidade de aplicar a técnica em ambas as fazes: compilação para o
ambiente e compilação para a máquina.
Para tal, torna-se fundamental, a estruturação dos códigos propostos, como o de
escalonamento de cópias e o cálculo da dominância de fronteiras, em uma linguagem
real, criando asssim a possibilidade de aplicá-las em um compilador e/ou ferramenta.
Com isto, será possı́vel realizar testes reais, uma avaliação empı́rica, e um debbug para a
melhor compreensão dos algoritmos.

12. Conclusão
A SSA é um conceito que simplifica a adoção de outras técnicas de otimização. Estas por
sua vez, implicam diretamente na otimização de códigos e na aplicação final, melhorando-
a em vários sentidos, como: tamanho final da aplicação, performance, melhor uso de
hardware etc.
Foi ressaltado que a SSA é uma técnica onde deve ocorrer um retorno para uma
representação simples do código, devido a falta de suporte (de hardware) para algumas
caracterı́sticas da forma SSA, como as φ-funcions.
Ficou claro também que um dos alicerces principais, da técninca SSA, está nas
φ-funcions, que garantem o suo correto de variáveis nas bifurcações do código.
O Artigo apresentou algumas das técnicas que se beneficiam da SSA, explicando
seus conceitos e ilustrando a evoluçã, partindo da conversão para a SSA até chegar na
otimização, como foi o casa do Code Motion.
Para tal, houve crı́ticas a autores citados no artigo por ignorarem as origens da
técnica e comprometer uma aplicação moderna da técnica, devido ao déficit da proposta
inicial da SSA.
Logo foi proposto um novo estudo, onde seriam analisados dados provenientes da
aplicação da SSA nos compiladores de plataformas como o JAVA e o C#, estudando o
comportamento do código e desempenho da aplicação, quando a técnica for aplicada na
geração de byetecode, por exemplo.
References
APPEL, Andrew W. (1998). Moder Compiler Implementation in C. England: Cambridge
University.
BRIGGS, Preston et. Al. (s\d) Practical Improvements to the Construction and Destruc-
tion of Static Single Assignment Form. Tera Computer Company.
CYTRON, R. et. Al. (1989) Efficiently computing static single assignment form and the
control dependence graph. ACM Trans.
GCC. Seção GNU Compiler Collection. Disponı́vel em: <http://gcc.gnu.org/>. Acesso
em: 5 nov. 2009 às 9h10.
Seção Conhecendo o GCC 4, 28 out. 2008. <http://www.ibm.com/developerworks/br/library/l-
gcc4/index.html>. Acesso em: 05 nov. 2009 às 8h.
MAYER, Sabine. (2003). Static Single-Assignment Form and Two Algorithms for its
Generation. University of Konstanz.
WIKIPEDIA. Seção Static Single Assignment Form. Disponı́vel em:
<http://en.wikipedia.org/wiki/Static single assignment form>. Acesso em: 5
nov. 2009 às 9h.
Figure 10. Algoritmo para escalonamento de inserção de cópias
Figure 11. Propagação de Constante

Figure 12. Eliminação de Redundância Parcial

Figure 13. Aplicação do Deslocamento de Código

Figure 14. Código Morto

Вам также может понравиться