J'ai un code qui ressemble plus ou moins à ceci:
#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 fait la chose intelligente et la compile en une seule instruction and
(qui est ensuite alignée partout ailleurs):
apply_known_mask(std::bitset<64ul>&): # @apply_known_mask(std::bitset<64ul>&)
and qword ptr [rdi], 775946532
ret
Mais chaque version de GCC que j'ai essayée compile cela en un énorme gâchis qui inclut la gestion des erreurs qui devrait être statiquement DCE. Dans un autre code, il placera même le important_bits
équivalent comme donnée conforme au code!
.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*, ...)
Comment dois-je écrire ce code pour que les deux compilateurs puissent faire la bonne chose? A défaut, comment dois-je écrire ceci pour qu'il reste clair, rapide et maintenable?
La meilleure version est c ++ 17 :
template< unsigned char... indexes >
constexpr unsigned long long mask(){
return ((1ull<<indexes)|...|0ull);
}
Ensuite
void apply_known_mask(std::bitset<64> &bits) {
constexpr auto m = mask<B,D,E,H,K,M,L,O>();
bits &= m;
}
de retour dans c ++ 14 , nous pouvons faire cette étrange astuce:
template< unsigned char... indexes >
constexpr unsigned long long mask(){
auto r = 0ull;
using discard_t = int[]; // data never used
// value never used:
discard_t discard = {0,(void(
r |= (1ull << indexes) // side effect, used
),0)...};
(void)discard; // block unused var warnings
return r;
}
ou, si nous sommes coincés avec c ++ 11 , nous pouvons le résoudre récursivement:
constexpr unsigned long long mask(){
return 0;
}
template<class...Tail>
constexpr unsigned long long mask(unsigned char b0, Tail...tail){
return (1ull<<b0) | mask(tail...);
}
template< unsigned char... indexes >
constexpr unsigned long long mask(){
return mask(indexes...);
}
Godbolt avec les - vous pouvez changer de définition CPP_VERSION et obtenir un assemblage identique.
En pratique, j'utiliserais le plus moderne possible. 14 bat 11 parce que nous n'avons pas de récursivité et donc de longueur de symbole O (n ^ 2) (ce qui peut exploser le temps de compilation et l'utilisation de la mémoire du compilateur); 17 bat 14 parce que le compilateur n'a pas à éliminer de code mort ce tableau, et cette astuce de tableau est tout simplement moche.
De ces 14, c'est le plus déroutant. Ici, nous créons un tableau anonyme de tous les 0, en attendant comme effet secondaire, construisons notre résultat, puis jetons le tableau. Le tableau mis au rebut contient un nombre de 0 égal à la taille de notre pack, plus 1 (que nous ajoutons pour pouvoir gérer les packs vides).
Une explication détaillée de ce que fait la version c ++ 14 . C'est une astuce/hack, et le fait que vous deviez le faire pour étendre les packs de paramètres avec efficacité en C++ 14 est l'une des raisons pour lesquelles les expressions de repli ont été ajoutées dans c ++ 17 .
Il est mieux compris de l'intérieur:
r |= (1ull << indexes) // side effect, used
cela met simplement à jour r
avec 1<<indexes
pour un index fixe. indexes
est un pack de paramètres, nous devrons donc l'étendre.
Le reste du travail consiste à fournir un pack de paramètres pour développer indexes
à l'intérieur de.
Un pas en avant:
(void(
r |= (1ull << indexes) // side effect, used
),0)
ici, nous convertissons notre expression en void
, indiquant que nous ne nous soucions pas de sa valeur de retour (nous voulons juste l'effet secondaire de la définition de r
- en C++, des expressions comme a |= b
renvoie également la valeur qu'ils ont définie a
sur).
Ensuite, nous utilisons l'opérateur virgule ,
et 0
pour supprimer la void
"valeur" et renvoyer la valeur 0
. Il s'agit donc d'une expression dont la valeur est 0
et comme effet secondaire du calcul de 0
, elle définit un bit dans r
.
int discard[] = {0,(void(
r |= (1ull << indexes) // side effect, used
),0)...};
À ce stade, nous développons le pack de paramètres indexes
. Nous obtenons donc:
{
0,
(expression that sets a bit and returns 0),
(expression that sets a bit and returns 0),
[...]
(expression that sets a bit and returns 0),
}
dans le {}
. Cette utilisation de ,
n'est pas l'opérateur virgule, mais plutôt le séparateur des éléments du tableau. Il s'agit de sizeof...(indexes)+1
0
s, qui définissent également des bits dans r
comme effet secondaire. Nous affectons ensuite les instructions de construction du tableau {}
à un tableau discard
.
Ensuite, nous convertissons discard
en void
- la plupart des compilateurs vous avertiront si vous créez une variable et ne la lisez jamais. Tous les compilateurs ne se plaindront pas si vous le transformez en void
, c'est en quelque sorte une façon de dire "Oui, je sais, je ne l'utilise pas", donc cela supprime l'avertissement.
L'optimisation que vous recherchez semble être le décollement de boucle, qui est activé à -O3
, ou manuellement avec -fpeel-loops
. Je ne sais pas pourquoi cela relève du pelage de boucle plutôt que du déroulement de la boucle, mais il n'est peut-être pas disposé à dérouler une boucle avec un flux de contrôle non local à l'intérieur (comme il y a, potentiellement, à partir de la vérification de la plage).
Par défaut, cependant, GCC ne peut pas décortiquer toutes les itérations, ce qui est apparemment nécessaire. Expérimentalement, en passant -O2 -fpeel-loops --param max-peeled-insns=200
(la valeur par défaut est 100) fait le travail avec votre code d'origine: https://godbolt.org/z/NNWrga
si vous utilisez uniquement C++ 11 est un must (&a)[N]
est un moyen de capturer des tableaux. Cela vous permet d'écrire une seule fonction récursive sans utiliser de fonctions d'assistance:
template <std::size_t N>
constexpr std::uint64_t generate_mask(Flags const (&a)[N], std::size_t i = 0u){
return i < N ? (1ull << a[i] | generate_mask(a, i + 1u)) : 0ull;
}
l'assigner à un constexpr auto
:
void apply_known_mask(std::bitset<64>& bits) {
constexpr const Flags important_bits[] = { B, D, E, H, K, M, L, O };
constexpr auto m = generate_mask(important_bits); //< here
bits &= m;
}
int main() {
std::bitset<64> b;
b.flip();
apply_known_mask(b);
std::cout << b.to_string() << '\n';
}
0000000000000000000000000000000000101110010000000000000100100100
// ^ ^^^ ^ ^ ^ ^
// O MLK H E D B
il faut vraiment apprécier la capacité de C++ à calculer tout ce qui est calculable au moment de la compilation. Cela me souffle sûrement encore l'esprit ( <> ).
Pour les versions ultérieures C++ 14 et C++ 17 yakk's réponse couvre déjà à merveille cela.
Je vous encourage à écrire un type EnumSet
approprié.
Écrire une base EnumSet<E>
en C++ 14 (à partir de) basé sur std::uint64_t
est trivial:
template <typename E>
class EnumSet {
public:
constexpr EnumSet() = default;
constexpr EnumSet(std::initializer_list<E> values) {
for (auto e : values) {
set(e);
}
}
constexpr bool has(E e) const { return mData & mask(e); }
constexpr EnumSet& set(E e) { mData |= mask(e); return *this; }
constexpr EnumSet& unset(E e) { mData &= ~mask(e); return *this; }
constexpr EnumSet& operator&=(const EnumSet& other) {
mData &= other.mData;
return *this;
}
constexpr EnumSet& operator|=(const EnumSet& other) {
mData |= other.mData;
return *this;
}
private:
static constexpr std::uint64_t mask(E e) {
return std::uint64_t(1) << e;
}
std::uint64_t mData = 0;
};
Cela vous permet d'écrire du code simple:
void apply_known_mask(EnumSet<Flags>& flags) {
static constexpr EnumSet<Flags> IMPORTANT{ B, D, E, H, K, M, L, O };
flags &= IMPORTANT;
}
En C++ 11, il nécessite quelques circonvolutions, mais reste néanmoins possible:
template <typename E>
class EnumSet {
public:
template <E... Values>
static constexpr EnumSet make() {
return EnumSet(make_impl(Values...));
}
constexpr EnumSet() = default;
constexpr bool has(E e) const { return mData & mask(e); }
void set(E e) { mData |= mask(e); }
void unset(E e) { mData &= ~mask(e); }
EnumSet& operator&=(const EnumSet& other) {
mData &= other.mData;
return *this;
}
EnumSet& operator|=(const EnumSet& other) {
mData |= other.mData;
return *this;
}
private:
static constexpr std::uint64_t mask(E e) {
return std::uint64_t(1) << e;
}
static constexpr std::uint64_t make_impl() { return 0; }
template <typename... Tail>
static constexpr std::uint64_t make_impl(E head, Tail... tail) {
return mask(head) | make_impl(tail...);
}
explicit constexpr EnumSet(std::uint64_t data): mData(data) {}
std::uint64_t mData = 0;
};
Et est invoqué avec:
void apply_known_mask(EnumSet<Flags>& flags) {
static constexpr EnumSet<Flags> IMPORTANT =
EnumSet<Flags>::make<B, D, E, H, K, M, L, O>();
flags &= IMPORTANT;
}
Même GCC génère trivialement une instruction and
à -O1
godbolt :
apply_known_mask(EnumSet<Flags>&):
and QWORD PTR [rdi], 775946532
ret
Depuis C++ 11, vous pouvez également utiliser la technique TMP classique:
template<std::uint64_t Flag, std::uint64_t... Flags>
struct bitmask
{
static constexpr std::uint64_t mask =
bitmask<Flag>::value | bitmask<Flags...>::value;
};
template<std::uint64_t Flag>
struct bitmask<Flag>
{
static constexpr std::uint64_t value = (uint64_t)1 << Flag;
};
void apply_known_mask(std::bitset<64> &bits)
{
constexpr auto mask = bitmask<B, D, E, H, K, M, L, O>::value;
bits &= mask;
}
Lien vers l'explorateur du compilateur: https://godbolt.org/z/Gk6KX1
L'avantage de cette approche sur la fonction constexpr du modèle est qu'elle est potentiellement légèrement plus rapide à compiler en raison de règle de Chiel .
Il y a ici des idées très "intelligentes". Vous ne contribuez probablement pas à la maintenabilité en les suivant.
est
{B, D, E, H, K, M, L, O};
tellement plus facile à écrire que
(B| D| E| H| K| M| L| O);
?
Ensuite, aucun du reste du code n'est nécessaire.