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.

Você vai aprender
  • O que o otimizador faz com leituras repetidas e laços "vazios".
  • Quando usar volatile: hardware, sinais (volatile sig_atomic_t) e setjmp; e quando não usar (threads).
  • Como ver a diferença no código de máquina com gcc -O2 -S.
  • O que restrict promete e o que acontece quando a promessa é quebrada.
  • Por que memcpy não aceita áreas que se sobrepõem e memmove aceita.

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çãoO que faz
movl sensor(%rip), %eaxmove long: copia o valor de sensor (da memória) para o registrador %eax
addl %eax, %eaxSoma %eax com ele mesmo
retVolta 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 -Wextra não avisar.
while (!parar)
Conta voltas até parar virar 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 parar a 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çãoUse
Variável mudada por um tratador de sinalvolatile sig_atomic_t
Registrador de hardware (placas, drivers)Ponteiro para volatile
Variável local que muda entre setjmp e longjmpvolatile
Variável dividida entre threads_Atomic ou mutex, não volatile
Variável comumNada: 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çãoSem 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

Um laço de espera (ou um programa com sinal) funciona com -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

EscritaO 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; C99Promessa: 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.cGera arquivo.s com o código de montagem otimizado
-WrestrictAviso 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:

sig_atomic_t terminou = 0;

2. Copiar dentro do mesmo array

Qual função copia corretamente quando origem e destino se sobrepõem?

(&v[1], &v[0], 4 * sizeof(int));

3. Onde vai o restrict

Complete o parâmetro para prometer que destino não se sobrepõe a nenhum outro ponteiro:

void copia(int * destino, const int *restrict origem, int n);

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.
  • volatile torna cada leitura e escrita observável: o compilador tem de fazer todas, na ordem. Dá para ver no gcc -O2 -S: duas leituras em vez de uma, um laço que relê em vez de um jmp para si mesmo.
  • Use volatile para hardware, para volatile sig_atomic_t em sinais e para locais que mudam entre setjmp e longjmp. Para threads, use _Atomic.
  • restrict (C99) é uma promessa de que um ponteiro é o único caminho até aquela memória. Quebrar a promessa é comportamento indefinido: a com_restrict(&x, &x) deu 1 em vez de 2 com -O2.
  • memcpy tem restrict e não aceita áreas sobrepostas; memmove aceita. O mesmo vale para snprintf, strcpy e outras: nunca passe a mesma memória como destino e origem.