Eu tenho um código que é mais ou menos assim:
#include <bitset>
enum Flags { A = 1, B = 2, C = 3, D = 5,
E = 8, F = 13, G = 21, H,
I, J, K, L, M, N, O };
void apply_known_mask(std::bitset<64> &bits) {
const Flags important_bits[] = { B, D, E, H, K, M, L, O };
std::remove_reference<decltype(bits)>::type mask{};
for (const auto& bit : important_bits) {
mask.set(bit);
}
bits &= mask;
}
Clang> = 3.6 faz a coisa certa e compila isso em uma única and
instrução (que então fica embutida em todos os outros lugares):
apply_known_mask(std::bitset<64ul>&): # @apply_known_mask(std::bitset<64ul>&)
and qword ptr [rdi], 775946532
ret
Mas cada versão do GCC que tentei compila isso em uma enorme bagunça que inclui tratamento de erros que devem ser estaticamente DCE. Em outro código, ele até colocará o important_bits
equivalente como dados em linha com o código!
.LC0:
.string "bitset::set"
.LC1:
.string "%s: __position (which is %zu) >= _Nb (which is %zu)"
apply_known_mask(std::bitset<64ul>&):
sub rsp, 40
xor esi, esi
mov ecx, 2
movabs rax, 21474836482
mov QWORD PTR [rsp], rax
mov r8d, 1
movabs rax, 94489280520
mov QWORD PTR [rsp+8], rax
movabs rax, 115964117017
mov QWORD PTR [rsp+16], rax
movabs rax, 124554051610
mov QWORD PTR [rsp+24], rax
mov rax, rsp
jmp .L2
.L3:
mov edx, DWORD PTR [rax]
mov rcx, rdx
cmp edx, 63
ja .L7
.L2:
mov rdx, r8
add rax, 4
sal rdx, cl
lea rcx, [rsp+32]
or rsi, rdx
cmp rax, rcx
jne .L3
and QWORD PTR [rdi], rsi
add rsp, 40
ret
.L7:
mov ecx, 64
mov esi, OFFSET FLAT:.LC0
mov edi, OFFSET FLAT:.LC1
xor eax, eax
call std::__throw_out_of_range_fmt(char const*, ...)
Como devo escrever este código para que ambos os compiladores possam fazer a coisa certa? Caso contrário, como devo escrever isso para que permaneça claro, rápido e sustentável?
c++
c++11
bit-manipulation
Alex Reinking
fonte
fonte
B | D | E | ... | O
?(1ULL << B) | ... | (1ULL << O)
(1ULL << Constant)
| por linha e alinhe os nomes das constantes nas diferentes linhas, isso seria mais fácil para os olhos.int
resultado da operação de bit PODE SERint
OU pode serlong long
dependendo do valor e formalmenteenum
não é equivalente a umaint
constante. clang pede "como se", gcc continua pedanteRespostas:
Melhor versão é c ++ 17:
Então
de volta c ++ 14, podemos fazer este truque estranho:
ou, se estivermos presos com c ++ 11, podemos resolvê-lo recursivamente:
Godbolt com todos os 3 - você pode mudar a definição CPP_VERSION e obter uma montagem idêntica.
Na prática, usaria o mais moderno que pudesse. 14 bate 11 porque não temos recursão e, portanto, comprimento de símbolo O (n ^ 2) (que pode explodir o tempo de compilação e o uso de memória do compilador); 17 supera 14 porque o compilador não precisa eliminar o código morto desse array, e esse truque do array é simplesmente feio.
Destes, 14 é o mais confuso. Aqui, criamos uma matriz anônima de 0s, enquanto isso, como efeito colateral, construímos nosso resultado e, em seguida, descartamos a matriz. O array descartado tem um número de 0s igual ao tamanho de nosso pacote, mais 1 (que adicionamos para que possamos lidar com pacotes vazios).
Uma explicação detalhada de como c ++ 14versão está fazendo. Este é um truque / hack, e o fato de você ter que fazer isso para expandir os pacotes de parâmetros com eficiência em C ++ 14 é uma das razões pelas quais expressões de dobra foram adicionadasc ++ 17.
É melhor compreendido de dentro para fora:
isso apenas atualiza
r
com1<<indexes
um índice fixo.indexes
é um pacote de parâmetros, então teremos que expandi-lo.O resto do trabalho é fornecer um pacote de parâmetros para expandir
indexes
dentro dele.Um passo para fora:
aqui, lançamos nossa expressão para
void
, indicando que não nos importamos com seu valor de retorno (queremos apenas o efeito colateral da configuraçãor
- em C ++, expressões comoa |= b
também retornam o valor definidoa
).Em seguida, usamos o operador vírgula
,
e0
para descartar ovoid
"valor" e retornar o valor0
. Portanto, esta é uma expressão cujo valor é0
e, como efeito colateral do cálculo0
, define um bit emr
.Neste ponto, expandimos o pacote de parâmetros
indexes
. Então temos:no
{}
. Este uso de não,
é o operador vírgula, mas sim o separador de elementos da matriz. Isso é s, que também define bits como efeito colateral. Em seguida, atribuímos as instruções de construção da matriz a uma matriz .sizeof...(indexes)+1
0
r
{}
discard
Em seguida, lançamos
discard
paravoid
- a maioria dos compiladores avisa se você cria uma variável e nunca a lê. Todos os compiladores não reclamarão se você lançar paravoid
, é uma espécie de maneira de dizer "Sim, eu sei, não estou usando isso", então suprime o aviso.fonte
((1ull<<indexes)|...|0ull)
, é uma "expressão dobrada" . Especificamente, é uma "dobra direita binária" e deve ser analisada como(pack
op
...
op
init)
A otimização que você está procurando parece ser o peeling de loop, que é ativado em
-O3
ou manualmente com-fpeel-loops
. Não tenho certeza de por que isso cai no campo de aplicação de peeling de loop em vez de desenrolamento de loop, mas possivelmente não está disposto a desenrolar um loop com fluxo de controle não local dentro dele (como há, potencialmente, na verificação de intervalo).Por padrão, entretanto, o GCC não consegue fazer o peeling de todas as iterações, o que aparentemente é necessário. Experimentalmente, a aprovação
-O2 -fpeel-loops --param max-peeled-insns=200
(o valor padrão é 100) faz o trabalho com seu código original: https://godbolt.org/z/NNWrgafonte
-O3 -fpeel-loops --param max-peeled-insns=200
falhe ... É devido ao-ftree-slp-vectorize
aparentemente.se usar apenas C ++ 11
(&a)[N]
é uma forma de capturar arrays. Isso permite que você escreva uma única função recursiva sem usar funções auxiliares de qualquer espécie:atribuindo-o a um
constexpr auto
:Teste
Resultado
realmente é preciso apreciar a capacidade do C ++ de calcular qualquer coisa que seja computável em tempo de compilação. Certamente ainda me impressiona ( <> ).
Para as versões posteriores C ++ 14 e C ++ 17, a resposta de yakk já cobre isso maravilhosamente.
fonte
apply_known_mask
realmente otimiza?constexpr
. E embora isso não seja teoricamente suficiente, sabemos que o GCC é perfeitamente capaz de avaliarconstexpr
como pretendido.Eu encorajaria você a escrever um
EnumSet
tipo adequado .Escrever um básico
EnumSet<E>
em C ++ 14 (em diante) baseado emstd::uint64_t
é trivial:Isso permite que você escreva um código simples:
No C ++ 11, isso requer algumas convoluções, mas continua sendo possível:
E é invocado com:
Até mesmo o GCC gera trivialmente uma
and
instrução em-O1
godbolt :fonte
constexpr
código não é legal. Quer dizer, alguns têm 2 afirmações! (C ++ 11 constexpr sugado)EnumSet<E>
não usa um valor deE
as valor diretamente, mas em vez disso usa1 << e
. É um domínio totalmente diferente, o que é realmente o que torna a classe tão valiosa => nenhuma chance de indexar acidentalmente por eme
vez de1 << e
.Desde C ++ 11, você também pode usar a técnica clássica de TMP:
Link para o Compiler Explorer: https://godbolt.org/z/Gk6KX1
A vantagem dessa abordagem sobre a função constexpr do template é que ela é potencialmente um pouco mais rápida de compilar devido à regra de Chiel .
fonte
Existem algumas idéias muito "inteligentes" aqui. Você provavelmente não está ajudando na manutenção ao segui-los.
é
muito mais fácil de escrever do que
?
Então, nada do resto do código é necessário.
fonte