Em programas simultâneos, os programadores prestarão atenção especial à sincronização de dados entre diferentes processos ou threads. Especialmente quando vários threads modificam a mesma variável ao mesmo tempo, uma sincronização confiável ou outras medidas devem ser tomadas para garantir que os dados sejam modificados corretamente. o ponto aqui é O princípio é: não assuma a ordem em que as instruções são executadas. Você não pode prever a ordem em que as instruções entre diferentes threads serão executadas.
Mas em um programa de thread único, geralmente é fácil assumirmos que as instruções são executadas sequencialmente; caso contrário, podemos imaginar que mudanças terríveis acontecerão no programa. O modelo ideal é: a ordem em que várias instruções são executadas é única e ordenada. Essa ordem é a ordem em que são escritas no código, independentemente do processador ou de outros fatores. é um modelo baseado no sistema von Neumann. É claro que esta suposição é razoável por si só e raramente ocorre de forma anormal na prática, mas na verdade, nenhuma arquitetura moderna de multiprocessador adota este modelo porque é simplesmente muito ineficiente. Na otimização de compilação e no pipeline de CPU, quase todos envolvem reordenação de instruções.
reordenação do tempo de compilação
Uma reordenação típica em tempo de compilação é ajustar a ordem das instruções para reduzir ao máximo o número de leituras e armazenamentos de registradores sem alterar a semântica do programa e reutilizar totalmente os valores armazenados dos registradores.
Suponha que a primeira instrução calcule um valor e o atribua à variável A e o armazene em um registrador. A segunda instrução não tem nada a ver com A, mas precisa ocupar um registrador (assumindo que ocupará o registrador onde A está localizado). instrução usa o valor de A e não tem nada a ver com a segunda instrução. Então, se de acordo com o modelo de consistência sequencial, A for colocado no registrador após a execução da primeira instrução, A não existirá mais quando a segunda instrução for executada, e A for lido no registrador novamente quando a terceira instrução for executada, e durante neste processo, o valor de A não mudou. Normalmente o compilador irá trocar as posições da segunda e terceira instruções, de modo que A exista no registrador no final da primeira instrução, e então o valor de A possa ser lido diretamente do registrador, reduzindo o overhead de leituras repetidas.
A importância do reordenamento para o pipeline
Quase todas as CPUs modernas usam o mecanismo de pipeline para acelerar o processamento de instruções. De modo geral, uma instrução requer vários ciclos de clock da CPU para ser processada e, por meio da execução paralela do pipeline, várias instruções podem ser executadas no mesmo ciclo de clock específico. O método é simplesmente declarado. Basta dividir as instruções em diferentes. O ciclo de execução, como leitura, endereçamento, análise, execução e outras etapas, é processado em diferentes componentes. Ao mesmo tempo, na unidade de execução EU, a unidade funcional é dividida em diferentes componentes, como componentes de adição, componentes de multiplicação. e componentes de carregamento, elementos de armazenamento, etc., podem realizar ainda mais a execução paralela de diferentes cálculos.
A arquitetura do pipeline determina que as instruções devem ser executadas em paralelo, e não conforme considerado no modelo sequencial. A reordenação conduz ao uso total do pipeline, alcançando assim efeitos superescalares.
Garanta a ordem
Embora as instruções não sejam necessariamente executadas na ordem em que as escrevemos, não há dúvida de que em um ambiente de thread único, o efeito final da execução da instrução deve ser consistente com seu efeito na execução sequencial, caso contrário, essa otimização perderá o significado.
Normalmente, os princípios acima serão satisfeitos quer a reordenação da instrução seja executada em tempo de compilação ou em tempo de execução.
Reordenando no modelo de armazenamento Java
No Java Memory Model (JMM), a reordenação é uma seção muito importante, especialmente na programação simultânea. JMM garante a semântica de execução sequencial por meio da regra acontece antes. Se você deseja que o thread que executa a operação B observe os resultados do thread que executa a operação A, então A e B devem satisfazer o princípio acontece antes. operações neles.
A palavra-chave volátil pode garantir a visibilidade das variáveis, porque as operações em voláteis estão todas na memória principal e a memória principal é compartilhada por todos os threads. O preço aqui é que o desempenho é sacrificado e os registros ou cache não podem ser usados porque não são globais. , a visibilidade não pode ser garantida e podem ocorrer leituras sujas.
Outra função do volátil é evitar localmente a reordenação. As instruções de operação em variáveis voláteis não serão reordenadas, pois se forem reordenadas, podem ocorrer problemas de visibilidade.
Em termos de garantia de visibilidade, bloqueios (incluindo bloqueios explícitos, bloqueios de objetos) e leitura e escrita de variáveis atômicas podem garantir a visibilidade das variáveis. No entanto, os métodos de implementação são ligeiramente diferentes. Por exemplo, o bloqueio de sincronização garante que os dados sejam relidos da memória para atualizar o cache quando o bloqueio for liberado, os dados serão gravados de volta na memória para garantir. que os dados são visíveis, enquanto variáveis voláteis simplesmente leem e gravam na memória.