Um guia para conjunto sequencial para C e C++

Conceitos básicos, intermediários e avançados

Primeiro, os autores descrevem a sintaxe de uso básica para conjunto sequencial integrado em programas C e C++. A seguir, eles explicam conceitos intermediários, como modos de endereçamento, lista de sobrescrever e sub-rotinas de ramificação, bem como tópicos mais avançados, como sobrescrever memória, atributo volátil e bloqueios, que são discutidos para aqueles que desejam usar conjunto sequencial em aplicativos multiencadeado.

Salma Elshatanoufy, Software Developer, IBM

Salma Elshatanoufy é desenvolvedora de software no grupo IBM XL Compilers, no Canadá. Salma trabalha na IBM, no departamento de testes, há três anos. Além de compiladores, seus interesses incluem aplicativos de encadeamentos múltiplos.



William O'Farrell, Software Developer, IBM Master Inventor, IBM

Bill O'Farrell é desenvolvedor de software no grupo IBM XL Compilers, no Canadá. Durante seus 20 anos na IBM, ele trabalhou em diversas áreas. Além de compiladores, seus interesses incluem depuradores e simultaneidade. Bill é doutor em computação paralela pela Syracuse University.



24/Nov/2011

Neste artigo, discutimos vários cenários de uso para conjunto sequencial, também chamado de inline asm. Para iniciantes, apresentamos sintaxe básica, referência de operando, restrições e armadilhas comuns das quais os novos usuários precisam estar cientes. Para usuários intermediários, discutimos a lista de sobrescrever, bem como tópicos de ramificação que facilitam o uso das instruções de ramificação dentro das sob-rotinas de conjunto sequencial no código C/C++. Por fim, discutimos sobrescrever memória e o atributo volátil para usuários avançados que usam conjunto sequencial para otimizar o código. Concluímos com um exemplo de bloqueio multiencadeado com conjunto sequencial.

Conjunto sequencial básico

No bloco de conjunto sequencial mostrado no código na Listagem 1, a instrução addc é usada para adicionar duas variáveis, op1 e op2. Em qualquer bloco de conjunto sequencial, as instruções de conjunto aparecem primeiro, seguidas pelas entradas e saídas, que são separadas por uma vírgula. As instruções de conjunto podem consistir em uma ou mais cadeias de caracteres entre aspas. A primeira vírgula separa os operandos de saída; a segunda vírgula separa os operandos de entrada. Se houver registradores de sobrescrever, eles são inseridos após a terceira vírgula. Se não houver entradas de sobrescrever para o bloco de conjunto sequencial, a terceira vírgula pode ser omitida, como a Listagem 2 mostra.

Listagem 1. Opcodes, entradas, saídas e sobrescrever

int res=0;
int op1=20;
int op2=30;

asm ( " addc.    %0,%1,%2        \n"         
       : "=r"(res)			             
       : "b"(op1), "r"(op2)                    
       : "r0" 					 );

Listagem 2. Nenhuma entrada de sobrescrita para o bloco do conjunto sequencial, então a terceira vírgula é omitida

asm ( " addc.    %0,%1,%2        \n"         
       : "=r"(res)			                 
       : "b"(op1), "r"(op2) 		 );

Observação:
A lista de sobrescrever é discutida mais adiante nesta seção.

Cada instrução "espera" que entradas e saídas sejam passadas em certo formato. No exemplo anterior, a instrução addc. espera que seus operandos sejam passados através de registros, assim, op1 e op2 são passados para o bloco do conjunto sequencial com as restrições "b" e "r" . Para uma listagem completa de todas as restrições de conjunto sequencial legais para o compilador IBM XL C e C++, consulte a referência de linguagem do compilador.

Registrar restrições nas declarações de variável

Em alguns programas, você desejará vincular variáveis a certos registros de hardware. Isso é feito na declaração de variável. O exemplo a seguir vincula a variável res à GPR0 em toda a vida do programa:

int register res asm("r0")=0;

Quando o tipo de variável não é combinado com o tipo de registro de hardware de destino, você receberá um aviso de erro de compilação.

Depois de uma variável ser vinculada a um registro específico, não é possível usar outro registro para reter a mesma variável. Por exemplo, o código a seguir causará um erro de compilação, a variável res é associada no tempo de declaração com GPR0, mas no bloco do conjunto sequencial, o usuário tenta usar qualquer registro, exceto GPR0, para passar para dentro res.

Listagem 3. Erro de compilação quando restrições conflitantes são usadas em uma variável

int register res asm("r0")=0;

asm ( " addc.    %0,%1,%2        \n"         
       : "=b"(res)			             
       : "b"(op1), "r"(op2)                    
       : "r0" 					 );

No exemplo na Listagem 4, não há operando saída para a instrução stw , assim, a seção de saídas do conjunto sequencial está vazia. Nenhum dos registros é modificado, então eles são todos operandos de entrada, e o endereço de destino é passado para dentro com os operandos de entrada. Entretanto, algo é modificado: o local da memória endereçada. Mas esse local não é explicitamente mencionado, então a saída da instrução é implícita, em vez de explícita.

Listagem 4. Instruções sem operandos de saída

int res [] = {0,0};
int a=45;
int *pointer = &res[0];

asm ( " stw    %0,%1(%2)        \n"            
	: 			  			      	                          
	: "r"(a), "i"(sizeof(int)),"r"(pointer));

Listagem 5. Instruções com operandos preservados

int res [] = {0,0};
int a=45;

asm ( " stw    %0,%1(%2)        \n"      
	: "+r"(res[0]) 
	: "r"(a), "i"(sizeof(int)),"r"(pointer));

Na Listagem 5, se desejar preservar o valor inicial de uma variável de resultado que não é necessariamente modificado pelo bloco do conjunto sequencial, é preciso usar a restrição + (sinal de mais) para preservar o valor inicial dessa variável, como mostrado em res[0].

Endereços de memória de destino no conjunto sequencial

Se uma instrução especificar dois dos seus argumentos em uma forma semelhante a D(RA), em que D é um valor literal e RA é um registro geral, isso é tomado para significar que D+RA é um endereço efetivo. Neste caso, as restrições adequadas são "m" ou "o". Tanto "m" quanto "o" se referem a argumentos de memória. A restrição "o" é descrita como um local de memória compensável . Mas na arquitetura IBM® POWER® , quase todas as referências de memória requerem uma compensação, então "m" e "o" são equivalentes. Neste caso, é possível usar uma única restrição para referir-se a dois operandos na instrução. A Listagem 6 é um exemplo.

Listagem 6. Uma única restrição para referir-se a dois operandos na instrução

int res [] = {0,0};
int a=45;

asm ( " stb %1,%0   	      \n"     	    
     : "=m"(res[1])                    
     : "r"(a));

A forma da instrução stb (da referência de linguagem do conjunto) é: stb RS,D(RA).

Embora a instrução stb tecnicamente tome três operandos (um registro de origem, um registro de endereço e um deslocamento imediato), a descrição do seu conjunto usa apenas duas restrições. A restrição "=m" é usada para notificar o compilador de que o endereço de memória de res deve ser usado para o resultado da instrução de armazenamento (a instrução "sync" é frequentemente usada para esse fim, mas há outras disponíveis, como descrito no IBM Support Assistant do POWER. Consulte os Recursos para um link). O "=m" indica que o operando é um local de memória modificado. Não é preciso conhecer o endereço do local antecipadamente, pois essa tarefa é deixada para o compilador. Isso permite ao compilador escolher o registro certo (r1 para uma variável automática, por exemplo) e aplicar o deslocamento certo automaticamente. Isso é necessário, pois geralmente seria impossível para um programado r de conjunto saber qual registro de endereço e qual deslocamento usar. Em outros casos, também é possível substituir esse comportamento calculando manualmente o endereço de destino como no seguinte exemplo.

Listagem 7. Calculando manualmente o endereço de destino

int res [] = {0,0};
int a=45;

asm ( " stb    %0,%1(%2)        \n"            
	: 			  			      	                          
: "r"(a), "i"(sizeof(int)),"r"(&res));

Nesse código, a especificação %1(%2) representa um endereço de base e uma compensação, em que %2 representa o endereço de base e res[0] e %1 representam a compensação o sizeof(int). Como resultado, o armazenamento é realizado no endereço efetivo, res.

Observação:
Para algumas instruções, GPR0 não pode ser usado como endereço de base. Especificar GPR0 diz ao assembler para não usar nenhum registro de base que seja. Para garantir que o compilador não escolha r0 para um operando, é possível usar a restrição "b" em vez de "r".

Modos de endereçamento para instruções POWER e PowerPC

O tipo de arquitetura IBM POWER é RISC. Instruções normalmente operam com três argumentos de registro (dois registros para argumentos de origem, um registro para deter um resultado) ou com dois registros e um valor imediato (um registro e um valor imediato para os argumentos de origem, e um registro para deter o resultado). Há exceções a esse padrão, mas de um modo geral ele é verdadeiro.

Entre as instruções que tomam dois registros e um valor imediato, há duas subclasses especiais: instruções de carregamento e instruções de armazenamento. Essas instruções usam o valor imediato como uma compensação ao valor no registro de origem para formar um "endereço efetivo". O valor de compensação é tipicamente uma compensação na pilha (r1 é o ponteiro da pilha), ou é uma compensação para o índice ―r2 é o ponteiro do índice). O índice é usado para promover o desenvolvimento de um código independente de posição que permite o carregamento dinâmico eficiente de bibliotecas compartilhadas nessas máquinas.

Ao usar um conjunto sequencial, não é preciso usar registros específicos nem desenvolver manualmente endereços efetivos. As restrições de argumento são usadas para direcionar o compilador para escolher registros ou desenvolver endereços efetivos adequados para os requisitos dessas instruções. Assim, se um registro geral for requerido pela instrução, é possível usar a restrição "r" ou "b" . A restrição "b" é de interesse porque muitas instruções usam a designação de registro 0 especialmente –- uma designação de registro 0 não significa que r0 é usado, mas, em vez disso, um valor literal de 0. Para essas instruções, é inteligente usar "b" para denotar os operandos de entrada para evitar que o compilador escolha r0. Se o compilador escolher r0, e a instrução tomar isso com um sentido literal de 0, a instrução produzirá resultados incorretos.

Listagem 8. r0 e seu significado especial na instrução stbx

char res[8]={'a','b','c','d','e','f','g','h'};
char a='y';
int index=7;

asm ("  stbx %0,%1,%2       \n"     
        :             
        : "r"(a), "r"(index), "r"(res) );

Aqui, a cadeia de caractere de resultado esperada é abcdefgy, mas se o compilador escolher r0 para %1, então o resultado seria, incorretamente, ybcdefgh. Para evitar que isso aconteça, use "b" como a Listagem 9 mostra.

Listagem 9. Usando a restrição "b" para significar GPR não zero

char res[8]={'a','b','c','d','e','f','g','h'};
char a='y';
int index=7;

asm ("  stbx %0,%1,%2       \n"     
        :             
        : "r"(a), "b"(index), "r"(res) );

Outro exemplo está no seguinte bloco de ASM. Embora pareça que o bloco de conjunto abaixo faça res=res+4, esse é o comportamento funcional real do código.

Listagem 10. Significado de r0 no segundo operando com o código de operação addi

int register res asm("r0")=5;
int b=4;

asm ( " addi    %0,%0,%1        \n"             
	 : "+r"(res)                     	                        
	 : "i"(b)		   				 
      : "r0");     	

where: addi    %0(result operand),%0(input operand res),%3(immediate operand b)

Já que res está vinculado a r0, a tradução do código do conjunto em visual do conjunto torna-se:
addi 0,0,4

O segundo operando não converte para registro zero. Em vez disso, ele converte para o número zero imediato. Na verdade, o seguinte é o resultado da operação addi:
res=0+4

Esse caso é especial para addi opcode. Se, em vez disso, res estivesse vinculado a r1, então o comportamento original pretendido teria sido obtido:
res=res+4


Lista de sobrescrever

Lista de sobrescrever básica

Em casos em que os registros não diretamente vinculados às entradas/saídas são usados dentro do bloco de conjunto, o usuário deve especificar esses registros dentro da lista de sobrescrever.

O clobbers list é usado para notificar o compilador de que os registros contidos dentro da lista podem, potencialmente, ter seus valores alterados. Assim, não devem ser usados para deter outros dados que não para as instruções para as quais são usados.

No exemplo na Listagem 11, os registros 8 e 7 são adicionados à lista de sobrescrever porque são usados nas instruções, mas não estão explicitamente vinculados a nenhum operando de entrada/saída. Ainda, o campo zero do registro de condição é adicionado à lista de sobrescrever pelo mesmo motivo. Embora não esteja presente nos operandos de entrada/saída, a instrução mfocrf lê esse bit do registro de condição e move o valor no registro 8.

Listagem 11. Exemplo de lista de sobrescrever

asm (" addc.    %0,%2,%3              \n"             
	" mfocrf   8,0x1                \n"                  
	" andi.    7,8,0xF              \n"             
     " stw      7,%1                  \n"             
	: "=r"(res),"=m"(c_bit)                                                  
        : : "b"(a), "r"(b)		    				      
	: "r0","r7","r8","cr0"      );     	  clobbers list

Se, em vez disso, a instrução mfocrf ler do campo 1 do registro de condição 1 (cr1), então esse campo precisaria ser adicionado à lista de sobrescrever, em vez disso. Ainda, o ponto no final das instruções addc. e andi. significa que seus resultados são comparados com zero, e o resultado da comparação é armazenado no campo 0 do registro de condição.

Quando os registros sobrescritos são omitidos da lista de sobrescrever, os resultados das operações de conjunto podem não estar corretos. Isso acontece porque esses registros de sobrescrever podem ser reutilizados para reter valores intermediários para outras operações. A menos que o compilador detecte que esses registros estão sobrescritos, os dados intermediários podem ser usados para realizar as instruções do programador, com resultados imprecisos. Ainda, as instruções de conjunto do usuário podem sobrescrever valores usados pelo compilador.

Exceções à lista de sobrescrever

Quase todos os registros podem ser sobrescritos, exceto por aqueles listados na Tabela 1.

Tabela 1. Registros que não podem ser sobrescritos
RegistroDescrição
r1ponteiro de pilha
r2ponteiro de índice
r11ponteiro de ambiente
r13ponteiro de dados local de encadeamento de modo de 64 bits
r30frequentemente usado pelo compilador como um ponteiro de estrutura da pilha, ponteiro para área constante
r31frequentemente usado pelo compilador como um ponteiro de estrutura da pilha, ponteiro para área constante

Sobrescrever memória

Sobrescrever memória implica uma, e também tem impacto sobre como o compilador trata alias de dados em potencial. Sobrescrever memória diz que o bloco do conjunto modifica a memória que de outra forma não é mencionada nas instruções do conjunto. Assim, por exemplo, um uso correto da memória seria ao usar uma instrução que limpa uma linha de cache. O compilador presumirá que praticamente quaisquer dados podem ser passar por alias com a memória alterada para aquela instrução. Como resultado, todos os dados requeridos usados após o bloco do conjunto serão recarregados a partir da memória após o conjunto ser concluído. Isso é muito mais dispendioso que a fence simples implícita pelo atributo "volátil" (discutido mais adiante).

Lembre-se de que, por que sobrescrever memória diz que qualquer coisa pode passar por alias, tudo que é usado precisa ser recarregado após o conjunto, independentemente de ter qualquer relação com o conjunto. Sobrescrever memória pode ser adicionado à lista de sobrescrever simplesmente usando a palavra "memória", em vez de um nome de registro.


Ramificação

Ramificação básica

A ramificação pode ser difícil com conjunto sequencial, porque é preciso conhecer o endereço da instrução para a qual ramificar antes do tempo de compilação. Embora isso não seja possível, é possível usar rótulos. Usando rótulos, o endereço de ramificação pode ser projetado com um identificador único que pode ser usado como endereço de ramificação de destino.

Dentro de um único arquivo de origem, os rótulos não podem ser repetidos dentro de um bloco de conjunto sequencial, nem dentro de blocos de conjunto vizinhos dentro da mesma origem. Em um dado programa, cada rótulo é exclusivo. Há uma exceção a essa regra, porém, e é que se você usar ramificação relativa (mais sobre isso adiante). Com ramificação relativa, mais de um rótulo com o mesmo identificador podem ser encontrados dentro do mesmo programa e dentro do mesmo bloco de conjunto.

Observação:
Os rótulos não podem ser usados no conjunto para definir macros em função de possíveis conflitos de namespace.

No exemplo na Listagem 12, a ramificação ocorre quando o bit LT, o bit 0 do registro de condição é definido. Se não estiver definido, então a ramificação não é tomada.

Listagem 12. Exemplo de uma ramificação tomada quando o bit LT de CR0 é definido (0x80000000)

asm ( "  addic. %0,%2,%4                      	\n"  	
      "  bc     0xC,0,here                        	\n"     	
      "  there: add %1,%2,%3                		\n"       
      "  here:  mul %0,%2,%3                		\n"       	
      :  "=r"(res),"=r"(res2)                                    
      :  "r" (a),"r"(b),"r"(c)                                          
      :  "cr0" );

Da mesma forma, uma ramificação poderia ocorrer se o bit GT (bit 1) do registro de condição estivesse definido, como o código na Listagem 13.

Listagem 13. Exemplo de ramificação tomada quando o bit GT de CR0 é definido (0x40000000)

asm ( "  addic. %0,%2,%4                      	\n"     	
      "  bc     0xC,1,here                        	\n"     	
      "  there: add %1,%2,%3                		\n"       
      "  here:  mul %0,%2,%3                		\n"       	
      :  "=r"(res),"=r"(res2)                                    
      :  "r" (a),"r"(b),"r"(c)                                         
      :  "cr0" );

Com o conjunto sequencial, é perfeitamente legal ramificar dentro do mesmo bloco de conjunto; entretanto, não é possível ramificar entre diferentes blocos de conjunto, mesmo se estiverem contidos na mesma origem.

Ramificação relativa

Como discutido anteriormente, a ramificação relativa permite reutilizar o nome de um rótulo mais de uma vez dentro do mesmo programa. É usado predominantemente, porém, para determinar a posição do endereço de destino com relação à instrução de ramificação. Esses são exemplos dos códigos de ramificação relativa que podem ser usados:

  • F -para frente
  • B -para trás

Observação:
Eles devem ter rótulos numéricos como sufixo para estarem sintaticamente corretos.

Neste exemplo (Listagem 14), observe que o endereço de destino é referido como "Hereb". Neste caso, usamos o rótulo do endereço de destino anexado com um sufixo que determina onde esse rótulo é colocado em relação à instrução de ramificação em si. O rótulo "Here" está localizado antes da instrução de ramificação, assim, o uso do sufixo "b" em "Hereb."

Listagem 14. Precisa de legenda

asm (	    "   10: lwarx %0,0,%2  	\n"
              "   cmpwi %0,0     		\n"
	         "   bne- 20f        		\n"
	         "   ori %0,%0,1    		\n"
	         "   stwcx. %0,0,%2 		\n"
	         "   bne- 10b        	\n"
	         "   sync          		\n"
	         "   ori  %1,%1,1   		\n"
	         "   20:                	\n"		    :)

O registro de condição

O registro de condição é usado para capturar informações sobre os resultados de certas instruções.

Para instruções de ponto não flutuantes com o sufixo ponto final (.) que define CR, o resultado da operação é comparado a zero.

  • Se o resultado for maior que zero, então o bit 1 do campo CR é definido (0x4).
  • Se for menor que zero, então o bit 0 é definido para (0x8).
  • Se o resultado for igual a zero, então o bit 2 é definido (0x2).

Para todas as instruções de comparação, os dois valores são comparados e qualquer campo CR pode ser definido (não apenas CR0). A Tabela 2 lista os bits e seus significados correspondentes (há oito desses conjuntos de 4 bits no registro de condição, chamados "cr0, cr1, cr2 … cr7").

Tabela 2. Bits de um campo CR e os significados de diferentes configurações
BitNomeDescrição
0LTRA < 0
1GTRA > 0
2 EQRA = 0
3UEstouro para operações de número inteiro.

Não ordenado, para operações de ponto flutuantes

Observação:
Para instruções de ponto flutuantes com um ponto de sufixo, CR1 é definido para os 4 bits superiores de FPSCR.


Bloqueando o atributo volátil

Tornar um bloco de conjunto sequencial "volátil" como neste exemplo garante que, conforme otimiza, o compilador não move nenhuma instrução acima ou abaixo do bloco de instruções de conjunto.

asm volatile("  addic. %0,%1,%2\n" : "=r"(res): "=r"(a),"r"(a))

Isso pode ser particularmente importante em casos em que o código está acessando a memória compartilhada. Isso será ilustrado na próxima seção sobre bloqueio multiencadeado.\


Bloqueio multiencadeado

Um dos usos mais comuns de conjunto sequencial é para escrever segmentos breves de instruções para gerenciar bloqueios multiencadeados. Em função do modelo de memória solta na arquitetura POWER, desenvolver esses bloqueios requer uso cuidadoso de algumas instruções:

  • Uma instrução que carrega a palavra de bloqueio e cria uma "reserva"
  • Outra que atualiza a palavra de bloqueio se a reserva não tiver sido perdida no meio tempo

Observação:
Se a reserva tiver sido perdida, um loop pode ser usado para tentar novamente repetidamente.

A Listagem 15 mostra uma função sequencial básica que tenta obter um bloqueio (hpa vários problemas com esse código, que discutiremos após estes exemplos).

Listagem 15. Exemplo da função de bloqueio Acquire no conjunto

inline bool acquireLock(int *lock){
	bool returnvalue = false;
	int lockval;
	asm (
	"0: lwarx %0,0,%2  \n" //load lock and reserve
	"   cmpwi 0,%0,0   \n" //compare the lock value to 0
	"   bne 1f         \n" //not 0 then exit function
	"   ori %0,%0,1    \n" //set the lock to 1
	"   stwcx. %0,0,%2 \n" //try to acquire the lock
	"   bne 0b         \n" //reservation lost, try again
	"   ori  %1,%1,1   \n" //set the return value to true
	"1:                \n" //didn't get lock, return false
	: "+r" (lockval), "+r" (returnvalue)
	: "r"(lock)            //parameter lock is an address
	: "cr0" );             //cmpwi, stwcx both clobber cr0
   return returnvalue;
}

A Listagem 16 é um exemplo de como essa função sequencial poderia ser usada.

Listagem 16. Exemplo de como a função acquireLock pode ser usada

if (acquireLock(lockWord)){
   //begin to use the shared region
   temp = x + 1;
    .  .  .
}

Porque a função é sequencial, o código resultante não terá uma chamada real nele. Em vez disso, ele o uso da região compartilhada x será antecedido pelas instruções para obter o bloqueio.

O primeiro problema a observar com esse código é a falta de uma instrução de sincronização. Um dos principais aprimoramentos de desempenho possibilitado pelo modelo de memória solta da arquitetura POWER a habilidade da máquina de reordenar cargas e armazenamentos para fazer um uso mais eficiente dos canais internos. Porém, às vezes o programador precisa abreviar esse reordenamento em algum grau para acessar adequadamente o armazenamento compartilhado. No caso de um bloqueio, você não desejaria que um carregamento de dados da região compartilhada ("x" no caso acima) fosse reordenado de modo que ocorresse antes de o bloqueio na região ser obtido. Por esse motivo, uma instrução de sincronização deve ser inserida para indicar à máquina que limite o reordenamento neste caso. A instrução sync muitas vezes é usada para esse fim, mas há outras opções disponíveis, como descrito no IBM Support Assistant do POWER (consulte os Recursos). No exemplo de código na Listagem 17, inserimos a instrução sync para evitar o reordenamento de carregamentos de "x" (isso é chamado de "barreira de importação"):

Listagem 17. Exemplo de sincronização

inline bool acquireLock(int *lock){
	bool returnvalue = false;
	int lockval;
	asm (
	"0: lwarx %0,0,%2  \n" //load lock and reserve
	"   cmpwi 0,%0,0   \n" //compare the lock value to 0
	"   bne 1f         \n" //not 0 then exit function
	"   ori %0,%0,1    \n" //set the lock to 1
	"   stwcx. %0,0,%2 \n" //try to acquire the lock
	"   bne 0b         \n" //reservation lost, try again
	"   sync          \n" //import barrier
	"   ori  %1,%1,1   \n" //set the return value to true
	"1:                \n" //didn't get lock, return false
	: "+r" (lockval), "+r" (returnvalue)
	: "r"(lock)            //parameter lock is an address
	: "cr0" );             //cmpwi, stwcx both clobber cr0
   return returnvalue;
}

Nesse bloco de conjunto, a sincronização evitará que quaisquer cargas subsequentes ocorram até que seja mostrado que caminho a ramificação anterior tomou. Assim, a variável x não será carregada, a menos que a ramificação não tenha sido tomada e acquireLock retorne true.

Então estamos prontos agora? Infelizmente não. Ainda precisamos nos preocupar com o que o compilador pode fazer.

Compiladores de otimização modernos podem ser muito agressivos ao movimentar o código ― e inclusive removê-lo completamente ― se parecer que as alterações podem fazer o programa operar mais rápido sem alterar a semântica do código. Entretanto, os compiladores normalmente não estão cientes das complexidades envolvidas no acesso à memória compartilhada. Por exemplo, um compilador pode não mover a instrução temp = x + 1; para um local superior no programa se determinar que o resultado seria planejado com mais eficiência (e presume que o "se" é sempre tomado). É claro, isso seria desastroso do ponto de vista de acessar os dados compartilhados. Para evitar o movimento de qualquer carregamento (ou qualquer instrução que seja) de abaixo do conjunto sequencial para um local acima dele, é possível usar a palavra-chave "volátil" (também conhecida como atributo volátil) para modificar o bloco do conjunto, como mostra a Listagem 18.

Listagem 18. Exemplo de palavra-chave volátil

inline bool acquireLock(int *lock){
	bool returnvalue = false;
	int lockval;
	asm volatile (
	"0: lwarx %0,0,%2  \n" //load lock and reserve
	. . .
	"1:                \n" //didn't get lock, return false
	: "+r" (lockval), "+r" (returnvalue)
	: "r"(lock)            //parameter lock is an address
	: "cr0" );             //cmpwi, stwcx both clobber cr0
   return returnvalue;
}

Quando isso é feito, uma fence interna é colocada antes e depois do bloco de conjunto, que impede que as instruções sejam movidas além dela. E lembre-se de que esse bloco do conjunto é sequencial, então evitará que o acesso a x seja movido acima do bloqueio implementado do conjunto.


Sobrescrever memória em bloqueio multiencadeado

A discussão de bloqueio multiencadeado não estaria completa sem mencionar sobrescrever memória. A palavra-chave memory frequentemente é adicionada à lista de sobrescrever nessas situações, embora nem sempre esteja claro por que ela seria necessária. O uso de memória na lista de sobrescrever signifique a memória é alterada de maneira imprevisível pelo bloco do conjunto.

Entretanto, modificações de memória no exemplo de bloqueio fornecido são bastante previsíveis. Apesar de o bloqueio de variável ser um ponteiro (que aponta para um local de bloqueio, isso não é mais imprevisível que a expressão "*lock" em um programa C. Nesses casos, um compilador comportado provavelmente associaria a expressão "*lock" a todas as variáveis do tipo adequado, então recarregaria corretamente quaisquer variáveis afetadas após o ponteiro ter sido usado para modificar os dados. Contudo, o uso de sobrescrever memória parece ser uma prática difundida, provavelmente conduzida por um excesso de cuidado ao lidar com regiões compartilhadas. Os programadores devem estar cientes, no entanto, das penalidades de desempenho envolvidas e das abordagens alternativas.

Quando um conjunto sequencial inclui "memória" na lista de sobrescrever, isso significa que qualquer variável no programa pode ter sido modificada pelo conjunto, de modo que ela deve ser recarregada antes de ser usada. Esse requisito pode minar os esforços de otimização por parte do compilador. Uma abordagem potencialmente mais leve seria tornar a região compartilhada volátil (além do bloco de conjunto em si). Tornar uma variável volátil significa que seu valor deve ser recarregado antes que ela seja usada em uma dada expressão. Se a região compartilhada em questão for uma estrutura de dados, como uma lista ou fila, isso garantirá que a estrutura atualizada seja recarregada após o bloqueio ser adquirido. Entretanto, todos os acessos de dados não compartilhados podem desfrutar do complemento total das otimizações do compilador.

Dica:

Se a estrutura de dados compartilhados for acessada por um ponteiro (digamos, *p), certifique-se de declarar o ponteiro de modo a indicar que é o objeto apontado para ele que é volátil, e não o ponteiro em si. Por exemplo, isso declara que a lista apontada para p é volátil:

volatile list *p

Agradecimentos

Obrigado, Ian McIntosh, Christopher Lapkowski, Jim McInnes e Jae Broadhurst. Cada um de vocês desempenhou um importante papel na publicação deste artigo.

Recursos

Aprender

Obter produtos e tecnologias

  • Faça download de uma versão gratuita do software Rational.
  • Avalie outros produtos de software da IBM da forma que melhor lhe convier: faça o download da versão de avaliação, experimente-o on-line, use-o em um ambiente de nuvem ou passe algumas horas no SOA Sandbox aprendendo a implementar a Arquitetura Orientada a Serviços de forma eficiente.

Discutir

Comentários

developerWorks: Conecte-se

Los campos obligatorios están marcados con un asterisco (*).


Precisa de um ID IBM?
Esqueceu seu ID IBM?


Esqueceu sua senha?
Alterar sua senha

Ao clicar em Enviar, você concorda com os termos e condições do developerWorks.

 


A primeira vez que você entrar no developerWorks, um perfil é criado para você. Informações no seu perfil (seu nome, país / região, e nome da empresa) é apresentado ao público e vai acompanhar qualquer conteúdo que você postar, a menos que você opte por esconder o nome da empresa. Você pode atualizar sua conta IBM a qualquer momento.

Todas as informações enviadas são seguras.

Elija su nombre para mostrar



Ao se conectar ao developerWorks pela primeira vez, é criado um perfil para você e é necessário selecionar um nome de exibição. O nome de exibição acompanhará o conteúdo que você postar no developerWorks.

Escolha um nome de exibição de 3 - 31 caracteres. Seu nome de exibição deve ser exclusivo na comunidade do developerWorks e não deve ser o seu endereço de email por motivo de privacidade.

Los campos obligatorios están marcados con un asterisco (*).

(Escolha um nome de exibição de 3 - 31 caracteres.)

Ao clicar em Enviar, você concorda com os termos e condições do developerWorks.

 


Todas as informações enviadas são seguras.


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=80
Zone=Rational
ArticleID=776311
ArticleTitle=Um guia para conjunto sequencial para C e C++
publish-date=11242011