Houve muitos palpites (um pouco ou totalmente) errados nos comentários sobre alguns detalhes / antecedentes para isso.
Você está olhando para a implementação otimizada de fallback C otimizada da glibc. (Para ISAs que não possuem uma implementação ASM escrita à mão) . Ou uma versão antiga desse código, que ainda está na árvore de fontes glibc. https://code.woboq.org/userspace/glibc/string/strlen.c.html é um navegador de código baseado na árvore glibc git atual. Aparentemente, ele ainda é usado por alguns destinos principais da glibc, incluindo o MIPS. (Obrigado @zwol).
Em ISAs populares como x86 e ARM, o glibc usa asm escrito à mão
Portanto, o incentivo para alterar qualquer coisa sobre esse código é menor do que você imagina.
Esse código de bithack ( https://graphics.stanford.edu/~seander/bithacks.html#ZeroInWord ) não é o que realmente é executado no seu servidor / desktop / laptop / smartphone. É melhor do que um loop ingênuo de bytes por vez, mas mesmo esse bithack é muito ruim comparado ao asm eficiente para CPUs modernas (especialmente x86, em que o AVX2 SIMD permite verificar 32 bytes com algumas instruções, permitindo de 32 a 64 bytes por relógio circule no loop principal se os dados estiverem quentes no cache L1d em CPUs modernas com carga vetorial 2 / clock e taxa de transferência de ALU, ou seja, para cadeias de tamanho médio onde a sobrecarga de inicialização não domina.)
O glibc usa truques de vinculação dinâmica para resolver strlen
para uma versão ideal para sua CPU, portanto, mesmo no x86 há uma versão SSE2 (vetores de 16 bytes, linha de base para x86-64) e uma versão AVX2 (vetores de 32 bytes).
O x86 possui uma transferência de dados eficiente entre registros vetoriais e de uso geral, o que o torna excepcionalmente (?) bom para usar o SIMD para acelerar funções em cadeias de comprimento implícito nas quais o controle de loop depende dos dados. pcmpeqb
/ pmovmskb
possibilita testar 16 bytes separados por vez.
O glibc possui uma versão do AArch64 como essa usando o AdvSIMD e uma versão para as CPUs do AArch64 em que os registradores vector-> GP interrompem o pipeline; portanto, ele realmente usa esse bithack . Mas usa contagem de zeros à esquerda para encontrar o byte-dentro-do registro, uma vez atingido, e tira proveito dos acessos desalinhados eficientes do AArch64 após a verificação do cruzamento de páginas.
Também relacionado: Por que esse código é 6.5x mais lento com as otimizações ativadas? tem mais alguns detalhes sobre o que é mais rápido ou mais lento no x86 asm, strlen
com um buffer grande e uma implementação simples do asm que pode ser boa para o gcc saber como incorporar. (Algumas versões do gcc são improdutivamente inline, o rep scasb
que é muito lento, ou um bithack de 4 bytes por vez como este. Portanto, a receita inline-strlen do GCC precisa ser atualizada ou desativada.)
O Asm não possui "comportamento indefinido" no estilo C ; é seguro acessar os bytes da memória da maneira que desejar e uma carga alinhada que inclua os bytes válidos não pode causar falhas. A proteção de memória ocorre com granularidade de página alinhada; acessos alinhados mais estreitos do que isso não podem cruzar o limite da página É seguro ler além do final de um buffer na mesma página em x86 e x64? O mesmo raciocínio se aplica ao código de máquina que esse C hack obtém compiladores para criar para uma implementação não-inline autônoma dessa função.
Quando um compilador emite código para chamar uma função não-inline desconhecida, ele deve assumir que a função modifica todas / todas as variáveis globais e qualquer memória para a qual possa ter um ponteiro. ou seja, tudo, exceto os locais que não tiveram escape de endereço, deve estar sincronizado na memória durante a chamada. Isso se aplica a funções escritas em asm, obviamente, mas também a funções de biblioteca. Se você não ativar a otimização do tempo do link, ela se aplicará a unidades de tradução separadas (arquivos de origem).
Por que isso é seguro como parte da glibc, mas não o contrário.
O fator mais importante é que isso strlen
não pode ser incorporado a mais nada. Não é seguro para isso; ele contém UB com alias estrito (leitura de char
dados por meio de um unsigned long*
). char*
é permitido alias qualquer outra coisa, mas o inverso não é verdadeiro .
Esta é uma função de biblioteca para uma biblioteca compilada antecipadamente (glibc). Ele não será incorporado com a otimização do tempo do link nos chamadores. Isso significa que apenas é necessário compilar o código de máquina seguro para uma versão independente do strlen
. Não precisa ser portátil / seguro C.
A biblioteca GNU C precisa compilar apenas com o GCC. Aparentemente, não há suporte para compilá-lo com clang ou ICC, mesmo que eles suportem extensões GNU. O GCC é um compilador antecipado que transforma um arquivo de origem C em um arquivo de objeto de código de máquina. Não é um intérprete; portanto, a menos que seja incorporado no tempo de compilação, os bytes na memória são apenas bytes na memória. ou seja, UB com alias estrito não é perigoso quando os acessos com tipos diferentes ocorrem em funções diferentes que não se alinham.
Lembre-se de que strlen
o comportamento é definido pelo padrão ISO C. Esse nome da função especificamente faz parte da implementação. Compiladores como o GCC até tratam o nome como uma função interna, a menos que você o use -fno-builtin-strlen
, portanto strlen("foo")
pode ser uma constante em tempo de compilação 3
. A definição na biblioteca é usada apenas quando o gcc decide realmente emitir uma chamada para ela em vez de incluir sua própria receita ou algo assim.
Quando o UB não está visível para o compilador no momento da compilação, você obtém um código de máquina são. O código da máquina precisa funcionar para o caso no-UB e, mesmo que você queira , não há como o ASM detectar quais tipos o chamador usou para colocar dados na memória apontada.
O Glibc é compilado em uma biblioteca estática ou dinâmica autônoma que não pode ser alinhada com a otimização do tempo do link. Os scripts de construção da glibc não criam bibliotecas estáticas "gordas" que contêm código de máquina + representação interna do gcc GIMPLE para otimização do tempo de link ao incorporar em um programa. (ou seja libc.a
, não participará da -flto
otimização do tempo do link no programa principal.) Construir o glibc dessa maneira seria potencialmente inseguro para os alvos que realmente o usam.c
.
Na verdade, como comentários @zwol, LTO não pode ser usado na construção de glibc si , por causa do código "frágil" como este, que poderia quebrar se inlining entre arquivos de origem glibc era possível. (Existem alguns usos internos de strlen
, por exemplo, talvez como parte da printf
implementação)
Isso strlen
faz algumas suposições:
CHAR_BIT
é um múltiplo de 8 . Verdadeiro em todos os sistemas GNU. O POSIX 2001 ainda garante CHAR_BIT == 8
. (Isso parece seguro para sistemas com CHAR_BIT= 16
ou 32
, como alguns DSPs; o loop de prólogo desalinhado sempre executará 0 iterações se sizeof(long) = sizeof(char) = 1
porque todo ponteiro está sempre alinhado e p & sizeof(long)-1
sempre é zero.) Mas se você tivesse um conjunto de caracteres não ASCII em que caracteres são 9 ou 12 bits de largura, 0x8080...
é o padrão errado.
- (talvez)
unsigned long
tem 4 ou 8 bytes. Ou talvez funcione para qualquer tamanho de unsigned long
até 8, e usa um assert()
para verificar isso.
Esses dois não são possíveis UB, eles são apenas não portáveis para algumas implementações em C. Esse código é (ou fazia parte ) da implementação C nas plataformas em que ele funciona, então tudo bem.
A próxima suposição é C C potencial:
- Uma carga alinhada que contém bytes válidos não pode falhar e é segura desde que você ignore os bytes fora do objeto que você realmente deseja. (É verdade em todos os sistemas GNU e em todas as CPUs normais porque a proteção de memória ocorre com granularidade de página alinhada. É seguro ler além do final de um buffer na mesma página em x86 e x64? Seguro em C quando o UB não é visível no momento da compilação. Sem incluir, é o caso aqui. O compilador não pode provar que a leitura após a primeira
0
é UB; pode ser uma char[]
matriz C contendo, {1,2,0,3}
por exemplo)
Esse último ponto é o que torna seguro ler além do final de um objeto C aqui. Isso é praticamente seguro, mesmo quando se alinha com os compiladores atuais, porque acho que atualmente eles não tratam que implicar um caminho de execução é inacessível. De qualquer forma, o aliasing estrito já é um obstáculo se você deixar isso em linha.
Em seguida, você terá problemas como a antiga memcpy
macro CPP insegura do kernel do Linux que usa a conversão de ponteiros para unsigned long
( gcc, alias estrito e histórias de horror ).
Isso strlen
remonta à época em que você podia se dar bem com coisas assim em geral ; costumava ser bastante seguro sem a ressalva "somente quando não embutido" antes do GCC3.
UB que só é visível quando se olha através dos limites de chamada / retenção não pode nos prejudicar. (por exemplo, chamando isso em a em char buf[]
vez de em uma matriz de unsigned long[]
conversão para a const char*
). Uma vez que o código da máquina está definido, ele está lidando com bytes na memória. Uma chamada de função não em linha deve assumir que o receptor lê toda / qualquer memória.
Escrevendo isso com segurança, sem UB com alias estrito
O atributo de tipo GCCmay_alias
fornece a um tipo o mesmo tratamento de alias-everything que char*
. (Sugerido por @KonradBorowsk). Atualmente, os cabeçalhos do GCC o usam para tipos de vetores SIMD x86 como __m128i
você sempre pode fazer com segurança _mm_loadu_si128( (__m128i*)foo )
. (Consulte `reinterpret_cast`ing entre o ponteiro do vetor de hardware e o tipo correspondente um comportamento indefinido? Para obter mais detalhes sobre o que isso faz e o que não significa.)
strlen(const char *char_ptr)
{
typedef unsigned long __attribute__((may_alias)) aliasing_ulong;
aliasing_ulong *longword_ptr = (aliasing_ulong *)char_ptr;
for (;;) {
unsigned long ulong = *longword_ptr++; // can safely alias anything
...
}
}
Você também pode usar aligned(1)
para expressar um tipo com alignof(T) = 1
.
typedef unsigned long __attribute__((may_alias, aligned(1))) unaligned_aliasing_ulong;
É uma maneira portátil de expressar uma carga de aliasing na ISOmemcpy
, com a qual os compiladores modernos sabem alinhar como uma única instrução de carga. por exemplo
unsigned long longword;
memcpy(&longword, char_ptr, sizeof(longword));
char_ptr += sizeof(longword);
Isso também funciona para cargas desalinhadas porque memcpy
funciona como se por char
um acesso de cada vez. Mas, na prática, os compiladores modernos entendem memcpy
muito bem.
O perigo aqui é que, se o GCC não souber ao certo se char_ptr
está alinhado por palavras, não o incluirá em algumas plataformas que podem não suportar cargas desalinhadas no asm. por exemplo, MIPS antes do MIPS64r6 ou ARM mais antigo. Se você recebeu uma chamada de função real memcpy
apenas para carregar uma palavra (e deixá-la em outra memória), isso seria um desastre. Às vezes, o GCC pode ver quando o código alinha um ponteiro. Ou após o loop char-a-time que atinge um limite ulongo, você pode usar
p = __builtin_assume_aligned(p, sizeof(unsigned long));
Isso não evita a possível UB de leitura após o objeto, mas com o GCC atual, isso não é perigoso na prática.
Por que a fonte C otimizada à mão é necessária: os compiladores atuais não são bons o suficiente
O ASM otimizado manualmente pode ser ainda melhor quando você deseja cada última gota de desempenho para uma função de biblioteca padrão amplamente usada. Especialmente para algo como memcpy
, mas também strlen
. Nesse caso, não seria muito mais fácil usar C com intrínsecos x86 para aproveitar o SSE2.
Mas aqui estamos falando de uma versão C ingênua versus bithack C sem nenhum recurso específico do ISA.
(Penso que podemos considerá-lo um dado strlen
amplamente usado para fazê-lo funcionar o mais rápido possível, é importante. Portanto, a questão é se podemos obter código de máquina eficiente a partir de fontes mais simples. Não, não podemos.)
O GCC e o clang atuais não são capazes de auto-vetorizar loops nos quais a contagem da iteração não é conhecida antes da primeira iteração . (por exemplo, deve ser possível verificar se o loop executará pelo menos 16 iterações antes de executar a primeira iteração). compiladores.
Isso inclui loops de pesquisa ou qualquer outro loop com um dependente de dados if()break
e um contador.
O ICC (compilador da Intel para x86) pode auto-vetorizar alguns loops de pesquisa, mas ainda faz ingênuo byte de cada vez para um C simples / ingênuo, strlen
como o libc do OpenBSD usa. ( Godbolt ). (Da resposta de @ Peske ).
Uma libc otimizada manualmente strlen
é necessária para o desempenho dos compiladores atuais . Ir 1 byte por vez (com desenrolar talvez 2 bytes por ciclo em CPUs superescalares) é patético quando a memória principal pode acompanhar cerca de 8 bytes por ciclo, e o cache L1d pode fornecer 16 a 64 por ciclo. (2x cargas de 32 bytes por ciclo nas modernas CPUs x86 tradicionais desde Haswell e Ryzen. Sem contar o AVX512, que pode reduzir a velocidade do relógio apenas para o uso de vetores de 512 bits; é por isso que a glibc provavelmente não tem pressa em adicionar uma versão do AVX512 . Embora com vectores de 256-bit, AVX512VL + BW mascarado comparar numa máscara e ktest
ou kortest
poderia fazer strlen
mais HyperThreading amigável por redução dos seus UOPs / iteração.)
Estou incluindo non-x86 aqui, esses são os "16 bytes". por exemplo, a maioria das CPUs AArch64 pode fazer pelo menos isso, eu acho, e algumas certamente mais. E alguns têm taxa de transferência de execução suficiente strlen
para acompanhar essa largura de banda de carga.
É claro que os programas que funcionam com cadeias grandes geralmente devem acompanhar os comprimentos para evitar a necessidade de refazer a localização frequente das cadeias C de comprimento implícito. Mas o desempenho de curto a médio porte ainda se beneficia de implementações escritas à mão, e tenho certeza que alguns programas acabam usando strlen em cadeias de comprimento médio.