C volatile e restrict
Quando você compila com -O2, o compilador reescreve o seu programa por dentro para ele ficar mais rápido: guarda valores em vez de reler, apaga contas que "não servem para nada", junta operações. Quase sempre isso é ótimo. Mas há duas situações em que você sabe algo que o compilador não sabe, e precisa contar para ele. volatile ("volátil") diz: "esta variável pode mudar sem o programa mexer nela; leia de verdade toda vez". restrict ("restrito") diz o contrário: "estes ponteiros nunca apontam para o mesmo lugar; pode otimizar à vontade". Esta aula mostra as duas palavras em ação, no código de máquina e nos resultados.
- O que o otimizador faz com leituras repetidas e laços "vazios".
- Quando usar
volatile: hardware, sinais (volatile sig_atomic_t) esetjmp; e quando não usar (threads). - Como ver a diferença no código de máquina com
gcc -O2 -S. - O que
restrictpromete e o que acontece quando a promessa é quebrada. - Por que
memcpynão aceita áreas que se sobrepõem ememmoveaceita.
Todos os programas desta aula foram compilados com o gcc 15.2 na máquina fictícia da Ana (usuária ana, computador notebook), na pasta ~/c/volatile-restrict, com o comando padrão do guia (gcc -std=c23 -Wall -Wextra arquivo.c -o programa; veja Compilar com gcc) e, muitas vezes, com -O2, que liga as otimizações. Esta é uma aula avançada: ajuda ter visto Ponteiros e Comportamento Indefinido.
A ideia: o caderninho do atendente
Na lojinha, a Ana pergunta ao atendente quantos cafés há no estoque. Ele vai ao depósito, conta 12 e anota num caderninho. Cinco minutos depois ela pergunta de novo. Um atendente esperto pensa: "ninguém vendeu nem comprou café desde a última vez", e responde 12 olhando o caderninho, sem ir ao depósito. É exatamente o que o compilador faz com -O2: ele guarda o valor num registrador (o "caderninho" do processador, muito mais rápido que a memória) em vez de reler.
- Mas e se o depósito tem uma porta para a rua, por onde o fornecedor entrega café sem passar pelo balcão? Aí o caderninho mente. A Ana precisa avisar: "o estoque de café é volátil; vá contar toda vez". Isso é
volatile. - Agora o contrário. A Ana pede: "anote o estoque de café na ficha A e o de açúcar na ficha B". Um atendente cuidadoso, depois de mexer na ficha B, vai reler a ficha A, porque talvez A e B sejam a mesma ficha. Se a Ana garantir "são fichas diferentes, sempre", ele não precisa reler. Isso é
restrict.
1. O que o otimizador faz com uma variável
Este programa mede quanto tempo um laço vazio leva, contando até 100 milhões, com clock() (veja Data e Hora):
#include <stdio.h>
#include <time.h>
int main(void) {
clock_t inicio = clock();
for (int i = 0; i < 100000000; i++) {
}
double segundos = (double) (clock() - inicio) / CLOCKS_PER_SEC;
printf("O laço levou %.2f s\n", segundos);
return 0;
}
$ gcc -std=c23 -Wall -Wextra laco.c -o laco $ ./laco O laço levou 0.25 s $ gcc -std=c23 -Wall -Wextra -O2 laco.c -o laco-O2 $ ./laco-O2 O laço levou 0.00 s « o laço sumiu!
Com -O2, o gcc percebeu que o laço não muda nada que alguém veja depois, e o apagou. Isso é permitido: o padrão só exige que o programa se comporte "como se" fizesse tudo o que está escrito, no que dá para observar (a saída, os arquivos, as variáveis volatile). Agora mude uma palavra, for (volatile int i = 0; …), e compile de novo com -O2:
$ gcc -std=c23 -Wall -Wextra -O2 laco2.c -o laco2-O2 $ ./laco2-O2 O laço levou 0.27 s « com volatile, o laço voltou
Toda leitura e toda escrita de uma variável volatile conta como "algo observável". O compilador é obrigado a fazer cada uma, na ordem, sem guardar no caderninho e sem apagar.
Experimente: esta caixa compila com -O2. Rode e veja o tempo. Depois troque int i por volatile int i e rode de novo. (O tempo exato depende do seu computador.)
#include <stdio.h>
#include <time.h>
int main(void) {
clock_t inicio = clock();
for (int i = 0; i < 100000000; i++) {
}
double segundos = (double) (clock() - inicio) / CLOCKS_PER_SEC;
printf("O laço levou %.2f s\n", segundos);
return 0;
}
volatile não é para deixar o programa mais lento de propósito
Ninguém escreve laços vazios num programa de verdade; este é só o jeito mais fácil de ver o efeito. O uso real de volatile é para valores que mudam por fora, como nas próximas seções.
2. volatile no código de máquina
Para ver o que o compilador fez, peça o código de montagem com -S (as instruções do processador, uma por linha; veja Como o C Vira Programa). Imagine que sensor é um termômetro ligado ao computador: cada leitura pode dar um valor diferente.
int sensor;
volatile int sensor_v;
int le_duas(void) {
return sensor + sensor;
}
int le_duas_v(void) {
return sensor_v + sensor_v;
}
O gcc -O2 -S leitura.c cria o arquivo leitura.s. O sed -n '/^le_duas:/,/ret/p' mostra só o pedaço de uma função, do nome até o ret (de return, "voltar"):
$ gcc -std=c23 -O2 -S leitura.c $ sed -n '/^le_duas:/,/ret/p' leitura.s le_duas: .LFB0: .cfi_startproc endbr64 movl sensor(%rip), %eax « lê sensor UMA vez addl %eax, %eax « soma o valor com ele mesmo ret $ sed -n '/^le_duas_v:/,/ret/p' leitura.s le_duas_v: .LFB1: .cfi_startproc endbr64 movl sensor_v(%rip), %eax « lê sensor_v... movl sensor_v(%rip), %edx « ...e lê DE NOVO addl %edx, %eax ret
| Instrução | O que faz |
|---|---|
movl sensor(%rip), %eax | move long: copia o valor de sensor (da memória) para o registrador %eax |
addl %eax, %eax | Soma %eax com ele mesmo |
ret | Volta para quem chamou, com o resultado em %eax |
.cfi_startproc, endbr64, .LFB0: | Marcações para o depurador e para a segurança; pode ignorar |
Sem volatile, o gcc leu sensor uma vez só e dobrou o valor: para ele, ler duas vezes seguidas dá sempre o mesmo. Com volatile, ele fez as duas leituras que você escreveu, porque o termômetro pode ter mudado entre uma e outra.
O laço de espera
Fica mais sério num laço que espera alguém de fora mudar uma variável:
int pronto;
volatile int pronto_v;
void espera(void) {
while (!pronto) {
}
}
void espera_v(void) {
while (!pronto_v) {
}
}
$ gcc -std=c23 -O2 -S espera.c $ sed -n '/^espera:/,/^\.LFE/p' espera.s espera: .LFB0: .cfi_startproc endbr64 movl pronto(%rip), %eax « lê pronto uma vez testl %eax, %eax je .L3 « se for 0, vai para .L3... ret .L3: jmp .L3 « ...e fica pulando para si mesmo PARA SEMPRE .cfi_endproc .LFE0: $ sed -n '/^espera_v:/,/ret/p' espera.s espera_v: .LFB1: .cfi_startproc endbr64 .p2align 4 .p2align 4 .p2align 3 .L6: movl pronto_v(%rip), %eax « lê pronto_v testl %eax, %eax je .L6 « se for 0, volta e LÊ DE NOVO ret
Sem volatile, o gcc leu pronto uma vez. Se era 0, ele raciocinou: "dentro do laço ninguém muda pronto, então vai ser 0 para sempre", e trocou o laço por jmp .L3, um pulo para o próprio lugar: um laço infinito que nunca mais olha a variável. Com volatile, cada volta relê pronto_v (o je .L6 volta para o movl). Se algo de fora mudar a variável, o laço termina.
3. Quando usar volatile
Hardware
Em placas como o Arduino e em programas que conversam com o hardware (o kernel do Linux, por exemplo), alguns endereços de memória não são memória: são registradores de dispositivos. Ler o endereço de um botão diz se ele está apertado agora; escrever no endereço de um LED o acende. Cada leitura e cada escrita tem de acontecer de verdade, na ordem. É para isso que o volatile foi criado:
// exemplo ilustrativo: o endereço depende da placa
volatile unsigned int *const botao = (volatile unsigned int *) 0x40020010;
while ((*botao & 1) == 0) {
// espera o botão ser apertado
}
Esse código só faz sentido numa placa com um botão naquele endereço; num computador comum, o programa morreria ao ler um endereço que não é seu. Leia o tipo da direita para a esquerda: botao é um ponteiro const (não muda de endereço) para um unsigned int volatile (o valor lá muda sozinho). Veja const e Ponteiros.
Sinais: volatile sig_atomic_t
Um sinal é um aviso que o sistema manda ao programa: o Ctrl+C, o fim de um alarme (veja Sinais). Quando chega, o programa para o que está fazendo, roda uma função de tratamento e volta. Para o compilador, essa função "nunca é chamada" pelo laço principal, então o que ela muda é invisível para ele. Este programa pede um alarme de 1 segundo e conta voltas até o alarme mudar parar:
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
int parar = 0;
void tratar(int sinal) {
(void) sinal;
parar = 1;
}
int main(void) {
signal(SIGALRM, tratar);
alarm(1);
long voltas = 0;
while (!parar) {
voltas++;
}
printf("Parou depois de %ld voltas.\n", voltas);
return 0;
}
- signal(SIGALRM, tratar)
- "Quando chegar o sinal de alarme, chame
tratar." - alarm(1)
- Pede ao sistema um alarme daqui a 1 segundo (do
unistd.h, que existe no Linux e no macOS, não no C puro). - (void) sinal;
- Diz ao compilador "sei que não uso este parâmetro", para o
-Wextranão avisar. - while (!parar)
- Conta voltas até
pararvirar 1.
$ gcc -std=c23 -Wall -Wextra alarme.c -o alarme $ ./alarme Parou depois de 370852492 voltas. $ gcc -std=c23 -Wall -Wextra -O2 alarme.c -o alarme-O2 $ timeout 3 ./alarme-O2 $ echo $? 124 « nunca parou: o timeout matou depois de 3 s
Sem otimização, funcionou: o alarme tocou, tratar mudou parar, o laço viu. Com -O2, foi o caso do espera.c: o laço lê parar uma vez e nunca mais. O alarme tocou e mudou a variável, mas ninguém olhou. O timeout 3 matou o programa (código 124). O conserto é declarar a variável com o tipo que o padrão recomenda para sinais:
volatile sig_atomic_t parar = 0;
$ gcc -std=c23 -Wall -Wextra -O2 alarme2.c -o alarme2-O2 $ ./alarme2-O2 Parou depois de 2141691903 voltas.
- volatile
- Obriga o laço a reler
parara cada volta. - sig_atomic_t
- Um tipo inteiro (do
signal.h) que o processador lê e escreve de uma vez só, sem o sinal poder chegar "no meio" da escrita. Atomic = "indivisível".
Repare: com -O2 e volatile, o programa deu quase 6 vezes mais voltas no mesmo segundo do que sem otimização. O volatile só obriga a reler parar; o resto continua otimizado. O editor do guia não tem sinais vindos de fora, então este exemplo fica só no terminal.
setjmp e longjmp
Variáveis locais que mudam entre um setjmp e um longjmp também precisam de volatile, por um motivo parecido. A aula setjmp e longjmp mostra o caso.
Quando não usar: threads
É comum ver volatile em variáveis divididas entre threads (partes do programa rodando ao mesmo tempo). Isso não basta: volatile não impede que duas threads escrevam ao mesmo tempo, nem garante que uma veja as escritas da outra na ordem certa. Para threads, use _Atomic (de stdatomic.h, C11) ou um mutex (veja Threads).
4. restrict: a promessa de não sobrepor C99
restrict só vale para ponteiros. Escrever int *restrict a é uma promessa sua ao compilador: "enquanto esta função roda, a memória que eu acesso por a não é acessada por nenhum outro ponteiro". Em troca, ele pode guardar valores no caderninho sem medo. Veja a diferença:
#include <stdio.h>
int sem_restrict(int *a, int *b) {
*a = 1;
*b = 2;
return *a;
}
int com_restrict(int *restrict a, int *restrict b) {
*a = 1;
*b = 2;
return *a;
}
int main(void) {
int x, y;
printf("ponteiros diferentes: %d %d\n", sem_restrict(&x, &y), com_restrict(&x, &y));
printf("o mesmo ponteiro: %d %d\n", sem_restrict(&x, &x), com_restrict(&x, &x));
return 0;
}
As duas funções escrevem 1 em *a, 2 em *b e devolvem *a. Se a e b apontam para lugares diferentes, a resposta é 1. Se apontam para o mesmo lugar, o 2 escreveu por cima do 1, e a resposta certa é 2. Veja o que sai:
$ gcc -std=c23 -Wall -Wextra promessa.c -o promessa
promessa.c: In function ‘main’:
promessa.c:18:80: warning: passing argument 1 to ‘restrict’-qualified parameter aliases with argument 2 [-Wrestrict]
18 | printf("o mesmo ponteiro: %d %d\n", sem_restrict(&x, &x), com_restrict(&x, &x));
| ^~ ~~
$ ./promessa
ponteiros diferentes: 1 1
o mesmo ponteiro: 2 2
$ gcc -std=c23 -Wall -Wextra -O2 promessa.c -o promessa-O2
promessa.c: In function ‘main’:
promessa.c:18:80: warning: passing argument 1 to ‘restrict’-qualified parameter aliases with argument 2 [-Wrestrict]
18 | printf("o mesmo ponteiro: %d %d\n", sem_restrict(&x, &x), com_restrict(&x, &x));
| ^~ ~~
$ ./promessa-O2
ponteiros diferentes: 1 1
o mesmo ponteiro: 2 1 « com -O2, a promessa quebrada deu resposta errada
O -Wall já avisou: passing argument 1 to ‘restrict’-qualified parameter aliases with argument 2 = "o argumento 1, passado a um parâmetro restrict, é um apelido do argumento 2" (alias, "apelido": dois nomes para a mesma memória). Sem otimização, as duas funções deram 2. Com -O2, a com_restrict deu 1. O código de máquina mostra por quê:
$ gcc -std=c23 -O2 -S promessa.c $ sed -n '/^sem_restrict:/,/ret/p' promessa.s sem_restrict: .LFB12: .cfi_startproc endbr64 movl $1, (%rdi) « *a = 1 movl $2, (%rsi) « *b = 2 movl (%rdi), %eax « relê *a, porque b pode ter mudado ret $ sed -n '/^com_restrict:/,/ret/p' promessa.s com_restrict: .LFB13: .cfi_startproc endbr64 movl $1, (%rdi) « *a = 1 movl $1, %eax « resposta = 1, sem reler: "b não mexe em a" movl $2, (%rsi) « *b = 2 ret
Com restrict, o gcc confiou na promessa e respondeu 1 direto, sem reler *a. Uma instrução a menos. Num laço que roda milhões de vezes, coisas assim fazem diferença, e é por isso que funções como memcpy usam restrict. Mas, se a promessa é falsa, a resposta sai errada: comportamento indefinido.
Experimente: esta caixa compila com -O2, e o Clang do editor faz a mesma otimização do gcc. Rode e veja a segunda linha: o mesmo ponteiro: 1, quando a resposta certa seria 2. Depois apague as duas palavras restrict e rode de novo: aparece o 2.
#include <stdio.h>
int com_restrict(int *restrict a, int *restrict b) {
*a = 1;
*b = 2;
return *a;
}
int main(void) {
int x, y;
printf("ponteiros diferentes: %d\n", com_restrict(&x, &y));
printf("o mesmo ponteiro: %d\n", com_restrict(&x, &x));
return 0;
}
No editor, sem -O2, a resposta é 2 com ou sem restrict. E o Clang não dá o aviso -Wrestrict: esse aviso é do gcc.
5. memcpy × memmove
As duas copiam n bytes de um lugar da memória para outro (veja Funções de String). A diferença está no protótipo:
void *memcpy(void *restrict destino, const void *restrict origem, size_t n); void *memmove(void *destino, const void *origem, size_t n);
O memcpy tem restrict: você promete que o destino e a origem não se sobrepõem, e ele copia do jeito mais rápido que conseguir. O memmove não pede promessa nenhuma: ele confere se as áreas se sobrepõem e, se sim, copia na ordem certa (de trás para a frente, se for preciso). Veja os dois "empurrando" um texto uma posição para a direita, com origem e destino sobrepostos:
#include <stdio.h>
#include <string.h>
int main(void) {
char a[] = "abcdefgh";
char b[] = "abcdefgh";
memmove(a + 1, a, 7);
memcpy(b + 1, b, 7);
printf("memmove: %s\n", a);
printf("memcpy: %s\n", b);
return 0;
}
$ gcc -std=c23 -Wall -Wextra memcpy.c -o memcpy
$ ./memcpy
memmove: aabcdefg
memcpy: aabcdefg « "certo", por sorte
$ gcc -std=c23 -Wall -Wextra -O2 memcpy.c -o memcpy-O2
In file included from /usr/include/string.h:580,
from memcpy.c:2:
In function ‘memcpy’,
inlined from ‘main’ at memcpy.c:8:5:
/usr/include/x86_64-linux-gnu/bits/string_fortified.h:29:10: warning: ‘__builtin_memcpy’ accessing 7 bytes at offsets 1 and 0 overlaps 6 bytes at offset 1 [-Wrestrict]
29 | return __builtin___memcpy_chk (__dest, __src, __len,
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
30 | __glibc_objsize0 (__dest));
| ~~~~~~~~~~~~~~~~~~~~~~~~~~
$ ./memcpy-O2
memmove: aabcdefg
memcpy: aabccdfg « errado: letras repetidas e uma sumiu
Com a + 1 como destino e a como origem, as áreas se sobrepõem em 6 bytes. O memmove deu aabcdefg nas duas compilações. O memcpy acertou sem otimização e errou com -O2 (aabccdfg), depois de o gcc avisar: accessing 7 bytes at offsets 1 and 0 overlaps 6 bytes at offset 1 = "acessando 7 bytes nas posições 1 e 0, que se sobrepõem em 6 bytes a partir da posição 1". No editor do guia, o memcpy sobreposto acerta nas duas compilações e não há aviso: é o tipo de erro que passa calado.
Regra: se as áreas podem se sobrepor (mexer dentro do mesmo array), use memmove. Se são certamente separadas (copiar de um array para outro), memcpy. Na dúvida, memmove: a diferença de velocidade é pequena.
Missão: a Ana quer pôr um cliente novo (o número 5) no começo da fila. Para isso, todos andam uma posição para trás. O laço abaixo faz isso do jeito errado: cada um copia o da frente já copiado. Rode e veja o problema. Troque o laço por um único memmove que empurre os 5 números uma posição, para a saída ser 5 10 20 30 40 50.
#include <stdio.h>
#include <string.h>
int main(void) {
int fila[6] = {10, 20, 30, 40, 50, 0};
int tamanho = 5;
for (int i = 0; i < tamanho; i++) {
fila[i + 1] = fila[i];
}
fila[0] = 5;
tamanho++;
for (int i = 0; i < tamanho; i++) {
printf("%d ", fila[i]);
}
printf("\n");
return 0;
}
#include <stdio.h>
#include <string.h>
int main(void) {
int fila[6] = {10, 20, 30, 40, 50, 0};
int tamanho = 5;
memmove(&fila[1], &fila[0], tamanho * sizeof(int));
fila[0] = 5;
tamanho++;
for (int i = 0; i < tamanho; i++) {
printf("%d ", fila[i]);
}
printf("\n");
return 0;
}
O laço errado deu 5 10 10 10 10 10: o 10 foi copiado para a posição 1, depois a posição 1 (já 10) para a 2, e assim por diante. É o mesmo problema que um memcpy sobreposto pode ter. O memmove resolve porque, quando o destino está depois da origem, ele copia de trás para a frente. O terceiro argumento é em bytes: 5 números × sizeof(int).
Outras funções com restrict
Desde o C99, muitas funções da biblioteca têm restrict nos parâmetros: strcpy, strcat, printf, snprintf, fgets, scanf… Em todas, passar a mesma memória em dois parâmetros restrict é comportamento indefinido. Um erro clássico é tentar acrescentar algo a um texto usando ele mesmo:
#include <stdio.h>
int main(void) {
char nome[32] = "Ana";
snprintf(nome, sizeof nome, "%s Souza", nome);
printf("%s\n", nome);
return 0;
}
$ gcc -std=c23 -Wall -Wextra sprintf.c -o sprintf
sprintf.c: In function ‘main’:
sprintf.c:5:36: warning: ‘ Souza’ directive output may be truncated writing 6 bytes into a region of size between 1 and 32 [-Wformat-truncation=]
5 | snprintf(nome, sizeof nome, "%s Souza", nome);
| ^~~~~~
sprintf.c:5:5: note: ‘snprintf’ output between 7 and 38 bytes into a destination of size 32
5 | snprintf(nome, sizeof nome, "%s Souza", nome);
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
sprintf.c:5:5: warning: ‘snprintf’ argument 4 overlaps destination object ‘nome’ [-Wrestrict]
sprintf.c:4:10: note: destination object referenced by ‘restrict’-qualified argument 1 was declared here
4 | char nome[32] = "Ana";
| ^~~~
$ ./sprintf
Souza « o "Ana" sumiu
‘snprintf’ argument 4 overlaps destination object ‘nome’ = "o argumento 4 do snprintf se sobrepõe ao destino nome". O snprintf começou a escrever em nome e apagou o "Ana" antes de lê-lo. O conserto é escrever num outro array. (O primeiro aviso, -Wformat-truncation, diz que o resultado pode não caber em 32 bytes; veja Texto ↔ Número.)
Formas diferentes: qual usar
| Situação | Use |
|---|---|
| Variável mudada por um tratador de sinal | volatile sig_atomic_t |
| Registrador de hardware (placas, drivers) | Ponteiro para volatile |
Variável local que muda entre setjmp e longjmp | volatile |
| Variável dividida entre threads | _Atomic ou mutex, não volatile |
| Variável comum | Nada: volatile só deixa o programa mais lento |
| Função que recebe dois ponteiros e você garante que nunca se sobrepõem (e quer velocidade) | restrict nos parâmetros |
| Na dúvida sobre sobreposição | Sem restrict; para copiar, memmove |
Para quem está começando, a regra é simples: você vai ler restrict nos protótipos da biblioteca muito mais do que escrever. Quando ler, entenda como "não passe a mesma memória aqui".
Quando dá errado
-O0 e trava com -O2
O que significa: a variável que o laço espera é mudada "por fora" (tratador de sinal, hardware), mas não é volatile. O otimizador leu uma vez e transformou o laço em infinito.
Como consertar: declare a variável como volatile sig_atomic_t (sinais) ou acesse o hardware por um ponteiro para volatile. Se quem muda é outra thread, use _Atomic.
warning: passing argument 1 to ‘restrict’-qualified parameter aliases with argument 2 [-Wrestrict]
O que significa: você passou o mesmo ponteiro (ou a mesma memória) em dois parâmetros restrict. A promessa foi quebrada: comportamento indefinido, que pode dar resposta errada só com -O2.
Como consertar: passe memórias diferentes. Se a função precisa aceitar a mesma memória nos dois, tire o restrict dela.
warning: ‘__builtin_memcpy’ accessing N bytes at offsets 1 and 0 overlaps …
O que significa: um memcpy com origem e destino sobrepostos. Pode funcionar com -O0 e errar com -O2 (o aabccdfg acima). O aviso só aparece quando o gcc consegue calcular as posições; muitas vezes não aparece.
Como consertar: troque por memmove, com os mesmos argumentos.
warning: ‘snprintf’ argument 4 overlaps destination object
O que significa: o texto que você está montando também é usado como argumento. O snprintf apaga o original antes de lê-lo.
Como consertar: monte o resultado num array separado (char completo[64];) e, se precisar, copie de volta depois.
error: invalid use of ‘restrict’
O que significa: o restrict só vale para ponteiros, e depois do *: int *restrict p. Escrever restrict int x ou restrict int *p não faz sentido.
Como consertar: ponha o restrict logo depois do *.
Referência rápida
| Escrita | O que significa |
|---|---|
volatile int x; | Toda leitura e escrita de x acontece de verdade, na ordem |
volatile sig_atomic_t parar; | O tipo certo para uma variável mudada por um tratador de sinal |
volatile int *p; | Ponteiro para um int volátil (o valor apontado muda sozinho) |
int *volatile p; | O próprio ponteiro é volátil (raro) |
int *restrict p; C99 | Promessa: só p acessa essa memória enquanto p existe |
memcpy(dest, orig, n) | Copia n bytes; as áreas não podem se sobrepor |
memmove(dest, orig, n) | Copia n bytes; as áreas podem se sobrepor |
gcc -O2 -S arquivo.c | Gera arquivo.s com o código de montagem otimizado |
-Wrestrict | Aviso do gcc (já no -Wall) quando a mesma memória vai para dois parâmetros restrict |
Exercícios
1. Sinal
Complete a declaração da variável que um tratador de sinal muda:
2. Copiar dentro do mesmo array
Qual função copia corretamente quando origem e destino se sobrepõem?
3. Onde vai o restrict
Complete o parâmetro para prometer que destino não se sobrepõe a nenhum outro ponteiro:
4. Sem volatile, um laço while (!pronto) { } trava com -O2 mesmo quando um sinal muda pronto. Por quê?
5. O que acontece se você passar o mesmo ponteiro para dois parâmetros restrict?
6. Duas threads usam a mesma variável contador. O que usar?
Missão final: o nome completo
Missão: o programa tenta acrescentar o sobrenome ao nome usando o próprio nome como argumento do snprintf, e o "Ana" some. Crie um segundo array, char completo[64];, monte o nome completo nele e mostre Cliente: Ana Souza.
#include <stdio.h>
int main(void) {
char nome[64] = "Ana";
snprintf(nome, sizeof nome, "%s Souza", nome);
printf("Cliente: %s\n", nome);
return 0;
}
#include <stdio.h>
int main(void) {
char nome[64] = "Ana";
char completo[64];
snprintf(completo, sizeof completo, "%s Souza", nome);
printf("Cliente: %s\n", completo);
return 0;
}
Resumo
- Com
-O2, o compilador guarda valores em registradores, apaga laços sem efeito e evita releituras. Ele só garante o que dá para observar. volatiletorna cada leitura e escrita observável: o compilador tem de fazer todas, na ordem. Dá para ver nogcc -O2 -S: duas leituras em vez de uma, um laço que relê em vez de umjmppara si mesmo.- Use
volatilepara hardware, paravolatile sig_atomic_tem sinais e para locais que mudam entresetjmpelongjmp. Para threads, use_Atomic. restrict(C99) é uma promessa de que um ponteiro é o único caminho até aquela memória. Quebrar a promessa é comportamento indefinido: acom_restrict(&x, &x)deu 1 em vez de 2 com-O2.memcpytemrestricte não aceita áreas sobrepostas;memmoveaceita. O mesmo vale parasnprintf,strcpye outras: nunca passe a mesma memória como destino e origem.