유지 관리가 가능하고 빠른 컴파일 타임 비트 마스크를 C ++로 작성하려면 어떻게해야합니까?


113

다음과 같은 코드가 있습니다.

#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 은 현명한 작업을 수행하고이를 단일 and명령어로 컴파일합니다 (그런 다음 다른 모든 곳에서 인라인 됨).

apply_known_mask(std::bitset<64ul>&):  # @apply_known_mask(std::bitset<64ul>&)
        and     qword ptr [rdi], 775946532
        ret

그러나 내가 시도한 GCC의 모든 버전은 이것을 정적으로 DCE해야하는 오류 처리를 포함하는 엄청난 혼란으로 컴파일합니다. 다른 코드에서는 코드 important_bits와 함께 동등한 데이터를 데이터로 배치합니다 !

.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*, ...)

두 컴파일러가 옳은 일을 할 수 있도록이 코드를 어떻게 작성해야합니까? 실패하면 명확하고 빠르며 유지 관리 할 수 ​​있도록 어떻게 작성해야합니까?


4
루프를 사용하는 대신 B | D | E | ... | O? 로 마스크를 구성 할 수 없습니다 .
HolyBlackCat

6
열거 형에는 이미 확장 된 비트가 아닌 비트 위치가 있으므로 할 수 있습니다(1ULL << B) | ... | (1ULL << O)
Alex Reinking 19

3
단점은 실제 이름이 길고 불규칙하며 모든 라인 노이즈가있는 마스크에 어떤 플래그가 있는지 확인하기가 쉽지 않다는 것입니다.
Alex Reinking 2019

4
@AlexReinking 당신은 그것을 만들 수 있습니다 (1ULL << Constant)| 각 줄에 상수 이름을 정렬하면 눈에 더 쉬울 것입니다.
einpoklum

여기서 문제는 unsigned type 사용 부족과 관련된 문제라고 생각하는데, GCC는 항상 부호 / 부호없는 하이브리드에서 오버플로 및 유형 변환에 대한 수정을 정적으로 버리는 데 문제가 있다고 생각합니다. 여기서 비트 시프트의 int결과는 비트 연산 의 결과 int일 수도 있고 long long값에 따라 달라질 수도 있습니다. 공식적으로 상수 enum와 동일하지 않습니다 int. clang은 "as if"를 요구하고 gcc는 현학적 인 상태를 유지합니다
Swift-Friday Pie

답변:


112

최고의 버전은 :

template< unsigned char... indexes >
constexpr unsigned long long mask(){
  return ((1ull<<indexes)|...|0ull);
}

그때

void apply_known_mask(std::bitset<64> &bits) {
  constexpr auto m = mask<B,D,E,H,K,M,L,O>();
  bits &= m;
}

다시 , 우리는이 이상한 트릭을 할 수 있습니다.

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;
}

또는 우리가 붙어 있다면 , 우리는 재귀 적으로 해결할 수 있습니다.

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...);
}

3 가지 모두 Godbolt -CPP_VERSION 정의를 전환하고 동일한 어셈블리를 얻을 수 있습니다.

실제로 나는 내가 할 수있는 가장 현대적인 것을 사용합니다. 재귀가없고 따라서 O (n ^ 2) 심볼 길이 (컴파일 시간과 컴파일러 메모리 사용량이 폭발적으로 증가 할 수 있음)가 없기 때문에 14는 11을 이깁니다. 컴파일러가 해당 배열을 데드 코드 제거 할 필요가 없기 때문에 17은 14를 이깁니다. 그 배열 트릭은보기 흉합니다.

이 중 14 개가 가장 혼란 스럽습니다. 여기에서는 모두 0으로 구성된 익명 배열을 만들고, 부작용으로 결과를 생성 한 다음 배열을 버립니다. 버려진 배열에는 팩의 크기와 동일한 0과 1 (빈 팩을 처리 할 수 ​​있도록 추가)이 있습니다.


무엇에 대한 자세한 설명 버전이하고 있습니다. 이것은 트릭 / 해킹이며, C ++ 14에서 효율적으로 매개 변수 팩을 확장하기 위해 이렇게해야한다는 사실이 fold 표현식이 추가 된 이유 중 하나입니다..

내부에서 가장 잘 이해됩니다.

    r |= (1ull << indexes) // side effect, used

이것은 고정 인덱스에 대해 업데이트 r됩니다 1<<indexes. indexes매개 변수 팩이므로 확장해야합니다.

나머지 작업은 indexes내부 를 확장 할 매개 변수 팩을 제공하는 것 입니다.

한 단계 더 :

(void(
    r |= (1ull << indexes) // side effect, used
  ),0)

여기에서 표현식을로 캐스트 void하여 반환 값에 대해 신경 쓰지 않는다는 것을 나타냅니다 ( rC ++에서는 설정의 부작용 만 원합니다. C ++에서는 a |= b설정 한 값도 반환합니다 a).

그런 다음 우리는 쉼표 연산자를 사용 ,하고 0폐기하는 void"가치", 값을 반환합니다 0. 따라서 이것은 값이 0이고 계산의 부작용 0으로 r.

  int discard[] = {0,(void(
    r |= (1ull << indexes) // side effect, used
  ),0)...};

이 시점에서 매개 변수 팩을 확장합니다 indexes. 그래서 우리는 다음을 얻습니다.

 {
    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),
  }

에서 {}. 이러한 사용 ,이다 하지 콤마 연산자가 아니라 어레이 소자 분리막. 이것은 부작용으로 sizeof...(indexes)+1 0비트를 설정하는 s r입니다. 그런 다음 {}배열 구성 지침을 배열에 할당합니다 discard.

다음으로 캐스팅 discard합니다 void-대부분의 컴파일러는 변수를 생성하고 읽지 않으면 경고합니다. 캐스트하면 모든 컴파일러가 불평하지 않습니다. void"예, 알고 있습니다. 이걸 사용하지 않습니다"라고 말하는 방식이므로 경고를 표시하지 않습니다.


38
미안하지만 그 C ++ 14 코드는 뭔가입니다. 나는 무엇을 모른다.
James

14
@James C ++ 17의 fold 표현식이 환영받는 이유에 대한 훌륭한 동기 부여 예제입니다. 그것과 유사한 트릭은 재귀없이 팩을 "인플레 이스"로 확장 할 수있는 효율적인 방법으로 밝혀졌으며 컴파일러는 최적화하기 쉽습니다.
Yakk-Adam Nevraumont

4
@ruben 멀티 라인 constexpr은 11에서 불법입니다
Yakk-Adam Nevraumont

6
나는 C ++ 14 코드를 확인하는 것을 볼 수 없습니다. 어쨌든 C ++ 11이 필요하기 때문에 C ++ 11을 고수 할 것입니다.하지만 사용할 수 있다고하더라도 C ++ 14 코드에는 설명이 필요하지 않습니다. 이 마스크는 항상 최대 32 개의 요소를 갖도록 작성 될 수 있으므로 O (n ^ 2) 동작에 대해 걱정하지 않습니다. 결국 n이 상수에 의해 경계가 지어지면 실제로 O (1)입니다. ;)
Alex Reinking

9
((1ull<<indexes)|...|0ull)그것을 이해하려는 사람들에게는 "접힌 표현" 입니다. 구체적으로는 "진 오른쪽 배"이며, 이는로서 해석되어야(pack op ... op init)
헨릭 한센

47

찾고있는 최적화는에서 활성화 -O3되거나에서 수동으로 활성화되는 루프 필링 인 것 같습니다 -fpeel-loops. 나는 이것이 루프 풀기보다는 루프 필링의 범위에 속하는 이유를 잘 모르겠지만 아마도 내부에 비 로컬 제어 흐름이있는 루프를 풀지 않을 것입니다 (잠재적으로 범위 검사에서).

하지만 기본적으로 GCC는 분명히 필요한 모든 반복을 벗길 수없는 상태에서 중지됩니다. 실험적으로 전달 -O2 -fpeel-loops --param max-peeled-insns=200하면 (기본값은 100) 원래 코드로 작업이 완료됩니다. https://godbolt.org/z/NNWrga


당신은 대단 합니다. 감사합니다! 나는 이것이 GCC에서 구성 가능한지 몰랐습니다! 왠지 -O3 -fpeel-loops --param max-peeled-insns=200실패 하지만 ... -ftree-slp-vectorize분명히 때문 입니다.
Alex Reinking

이 솔루션은 x86-64 대상으로 제한되는 것 같습니다. ARM 및 ARM64의 출력은 여전히 ​​좋지 않으므로 OP와 완전히 관련이 없을 수 있습니다.
실시간으로

@realtime-실제로 다소 관련이 있습니다. 이 경우에는 작동하지 않는다고 지적 해 주셔서 감사합니다. GCC가 플랫폼 별 IR로 낮추기 전에이를 파악하지 못한 것은 매우 실망스러운 일입니다. LLVM 은 더 낮추기 전에 이를 최적화합니다 .
Alex Reinking

10

C ++ 11 만 사용해야 (&a)[N]하는 경우 배열을 캡처하는 방법입니다. 이렇게하면 도우미 함수를 사용하지 않고도 하나의 재귀 함수를 작성할 수 있습니다.

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;
}

에 할당 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

컴파일 타임에 계산 가능한 모든 것을 계산할 수있는 C ++의 능력에 감사해야합니다. 그것은 분명히 내 마음을 날려 버립니다 ( <> ).


이후 버전의 경우 C ++ 14 및 C ++ 17 yakk의 답변은 이미 훌륭하게 다루고 있습니다.


3
이것이 apply_known_mask실제로 최적화 된다는 것을 어떻게 증명 합니까?
Alex Reinking 2019

2
@AlexReinking : 모든 무서운 부분은 constexpr. 이론적으로는 충분하지 않지만 GCC가 constexpr의도 한대로 평가할 수 있다는 것을 알고 있습니다.
MSalters

8

적절한 EnumSet유형 을 작성하는 것이 좋습니다 .

EnumSet<E>C ++ 14 (이후)에서 기초를 작성하는 std::uint64_t것은 간단합니다.

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;
};

이를 통해 간단한 코드를 작성할 수 있습니다.

void apply_known_mask(EnumSet<Flags>& flags) {
    static constexpr EnumSet<Flags> IMPORTANT{ B, D, E, H, K, M, L, O };

    flags &= IMPORTANT;
}

C ++ 11에서는 일부 회선이 필요하지만 그럼에도 불구하고 여전히 가능합니다.

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;
};

그리고 다음과 함께 호출됩니다.

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;
}

GCC조차도 godboltand 에서 명령을 생성합니다 .-O1

apply_known_mask(EnumSet<Flags>&):
        and     QWORD PTR [rdi], 775946532
        ret

2
C ++ 11 에서는 대부분의 constexpr코드가 합법적이지 않습니다. 내 말은, 일부는 2 개의 진술을 가지고 있습니다! (C ++ 11 constexpr
sucked

@ Yakk-AdamNevraumont : 내가 가지 버전의 코드를 게시했다는 사실을 알고 계 셨나요 ? (제한 사항을 설명하기 위해)
Matthieu M.

1
std :: uint64_t 대신 std :: underlying_type을 사용하는 것이 더 나을 수 있습니다.
James

@James : 사실, 아닙니다. 참고 수행 EnumSet<E>의 값을 사용하지 않습니다 E직접적으로 값을 대신 사용을 1 << e. 그것은 완전히 다른 도메인이기 때문에 실제로 클래스를 매우 가치있게 만듭니다 => e대신 1 << e.
Matthieu M.

@MatthieuM. 네 말이 맞아. 나는 당신과 매우 유사한 우리 자신의 구현과 혼동하고 있습니다. (1 << e) 사용의 단점은 e가 기본 유형의 크기에 대한 경계를 벗어난 경우 아마도 UB 일 수 있으며 컴파일러 오류 일 수 있다는 것입니다.
James

7

C ++ 11부터 고전적인 TMP 기술을 사용할 수도 있습니다.

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;
}

컴파일러 탐색기 링크 : https://godbolt.org/z/Gk6KX1

템플릿 constexpr 함수에 비해이 접근 방식의 장점 은 Chiel 규칙 으로 인해 컴파일하는 것이 잠재적으로 약간 더 빠르다는 것 입니다.


1

여기에는 '영리한'아이디어가 있습니다. 당신은 아마도 그들을 따라가는 것으로 유지 관리를 돕지 않을 것입니다.

이다

{B, D, E, H, K, M, L, O};

작성하기가 훨씬 쉽습니다.

(B| D| E| H| K| M| L| O);

?

그러면 나머지 코드는 필요하지 않습니다.


1
"B", "D"등은 플래그 자체가 아닙니다.
Michał Łoś

예, 먼저이를 플래그로 변환해야합니다. 내 대답에는 전혀 명확하지 않습니다. 죄송합니다. 업데이트하겠습니다.
ANone 2019
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.