Por que os compiladores C ++ não otimizam essa atribuição booleana condicional como uma atribuição incondicional?

117

Considere a seguinte função:

void func(bool& flag)
{
    if(!flag) flag=true;
}

Parece-me que se o sinalizador tiver um valor booleano válido, isso seria equivalente a defini-lo truecomo incondicional , assim:

void func(bool& flag)
{
    flag=true;
}

No entanto, nem o gcc nem o clang o otimizam dessa maneira - ambos geram o seguinte no -O3nível de otimização:

_Z4funcRb:
.LFB0:
    .cfi_startproc
    cmp BYTE PTR [rdi], 0
    jne .L1
    mov BYTE PTR [rdi], 1
.L1:
    rep ret

Minha pergunta é: é apenas que o código é muito especial para cuidar de otimizar, ou há algum bom motivo para tal otimização ser indesejada, visto que flagnão é uma referência volatile? Parece que a única razão que pode ser é que flagpoderia, de alguma forma, ter um não- trueou- falsevalor sem comportamento indefinido no momento da leitura, mas não tenho certeza se isso é possível.

Ruslan
fonte
8
Você tem alguma evidência de que é uma "otimização"?
David Schwartz
1
@ 200_success Não acho que seja bom colocar uma linha de código com marcação não funcional como título. Se você quiser um título mais específico, ótimo, mas escolha uma frase em inglês e tente evitar código nela (por exemplo, por que os compiladores não otimizam gravações condicionais para gravações incondicionais quando podem provar que são equivalentes? Ou semelhantes). Além disso, como os crases não são renderizados, não os use no título, mesmo se você usar código.
Bakuriu
2
@Ruslan, embora não pareça fazer essa otimização para a própria função, quando pode embutir o código, parece fazer isso para a versão embutida. Freqüentemente, resulta apenas em uma constante de tempo de compilação 1sendo usada. godbolt.org/g/swe0tc
Evan Teran

Respostas:

102

Isso pode impactar negativamente o desempenho do programa devido a considerações de coerência do cache . Escrever para flagcada vez que func()for chamado sujaria a linha de cache que o contém. Isso acontecerá independentemente do fato de que o valor que está sendo escrito corresponde exatamente aos bits encontrados no endereço de destino antes da escrita.


EDITAR

hvd forneceu outra boa razão que impede essa otimização. É um argumento mais convincente contra a otimização proposta, uma vez que pode resultar em um comportamento indefinido, enquanto minha resposta (original) abordava apenas aspectos de desempenho.

Após um pouco mais de reflexão, posso propor mais um exemplo de por que os compiladores devem ser fortemente proibidos - a menos que eles possam provar que a transformação é segura para um contexto particular - de introduzir a gravação incondicional. Considere este código:

const bool foo = true;

int main()
{
    func(const_cast<bool&>(foo));
}

Com uma gravação incondicional, func()isso definitivamente aciona um comportamento indefinido (gravar na memória somente leitura encerrará o programa, mesmo se o efeito da gravação for de outra forma um no-op).

Leon
fonte
7
Também pode impactar positivamente no desempenho, já que você se livra de um branch. Portanto, não acho que seja significativo discutir esse caso específico sem um sistema muito específico em mente.
Lundin,
3
A definição do comportamento do @Yakk não é afetada pela plataforma de destino. Dizer que ele encerrará o programa é incorreto, mas o próprio UB pode ter consequências de longo alcance, incluindo demônios nasais.
John Dvorak
16
@Yakk Isso depende do que se entende por "memória somente leitura". Não, não está em um chip ROM, mas frequentemente está em uma seção carregada em uma página que não tem acesso de gravação habilitado, e você obterá, por exemplo, um sinal SIGSEGV ou exceção STATUS_ACCESS_VIOLATION quando tentar escrever nela.
Random832
5
"isso definitivamente desencadeia um comportamento indefinido". Não. O comportamento indefinido é uma propriedade da máquina abstrata. É o que o código diz que determina se o UB está presente. Compiladores não podem causar isso (embora, se houver erros, um compilador pode fazer com que os programas se comportem incorretamente).
Eric M Schmidt
7
É a rejeição de constpassar para uma função que pode modificar os dados que são a origem do comportamento indefinido, não da gravação incondicional. Doutor, dói quando eu faço isso ....
Spencer
48

Além da resposta de Leon sobre o desempenho:

Suponha que flagé true. Suponha que dois threads estejam constantemente chamando func(flag). A função conforme escrita, nesse caso, não armazena nada em flag, portanto, deve ser thread-safe. Dois threads acessam a mesma memória, mas apenas para lê-la. Definido incondicionalmente flagcomo truesignifica que dois threads diferentes estariam gravando na mesma memória. Isso não é seguro, isso não é seguro, mesmo se os dados que estão sendo gravados forem idênticos aos dados que já estão lá.


fonte
9
Acho que isso é resultado da aplicação [intro.races]/21.
Griwes
10
Muito interessante. Portanto, eu li isso como: O compilador nunca tem permissão para "otimizar" uma operação de gravação onde a máquina abstrata não teria uma.
Martin Ba
3
@MartinBa Principalmente. Mas se o compilador puder provar que isso não importa, por exemplo, porque pode provar que nenhuma outra thread poderia ter acesso a essa variável em particular, então pode estar tudo bem.
13
Isso só é inseguro se o sistema ao qual o compilador está direcionado o tornar inseguro . Nunca desenvolvi um sistema em que a gravação 0x01de um byte que já está 0x01causando um comportamento "inseguro". Em um sistema com acesso à memória word ou dword, isso aconteceria; mas o otimizador deve estar ciente disso. Em um PC moderno ou sistema operacional de telefone, nenhum problema ocorre. Portanto, este não é um motivo válido.
Yakk - Adam Nevraumont
4
@Yakk Na verdade, pensando ainda mais, acho que está certo afinal, mesmo para processadores comuns. Acho que você está certo quando a CPU pode gravar na memória diretamente, mas suponha que flagesteja em uma página de cópia na gravação. Agora, no nível da CPU, o comportamento pode ser definido (falha de página, deixe o sistema operacional lidar com isso), mas no nível do sistema operacional, ele ainda pode estar indefinido, certo?
13

Não tenho certeza sobre o comportamento de C ++ aqui, mas em C a memória pode mudar porque se a memória contiver um valor diferente de zero diferente de 1, ela permaneceria inalterada com a verificação, mas mudaria para 1 com a verificação.

Mas como não sou muito fluente em C ++, não sei se essa situação é possível.

glglgl
fonte
Isso ainda seria verdade _Bool?
Ruslan
5
Em C, se a memória contém um valor que a ABI não diz ser válido para seu tipo, então é uma representação de trap, e ler uma representação de trap é um comportamento indefinido. Em C ++, isso só poderia acontecer durante a leitura de um objeto não inicializado, e está lendo um objeto não inicializado que é UB. Mas se você puder encontrar um ABI que diga que qualquer valor diferente de zero é válido para o tipo bool/ _Boole significa true, então nesse ABI em particular, você provavelmente está certo.
1
@Ruslan Com compiladores que usam o Itanium ABI e em processadores ARM, C _Boole C ++ boolsão do mesmo tipo ou tipos compatíveis que seguem as mesmas regras. Com o MSVC, eles têm o mesmo tamanho e alinhamento, mas não há uma declaração oficial sobre se eles usam as mesmas regras.
Justin Time - Reintegrar Monica
1
@JustinTime: C's <stdbool.h>inclui um typedef _Bool bool; E sim, em x86 (pelo menos no System V ABI), bool/ _Booldevem ser 0 ou 1, com os bits superiores do byte apagados. Não acho que essa explicação seja plausível.
Peter Cordes
1
@JustinTime: É verdade, eu deveria apenas ter apontado que ele definitivamente tem a mesma semântica em todos os sabores x86 da ABI do System V, que é sobre o que esta pergunta se trata. (Posso dizer porque o primeiro argumento funcfoi passado em RDI, enquanto o Windows usaria RDX).
Peter Cordes