ビット 1 をカウントする popcnt やエンディアンを変換する bswap など x64 の高機能なビット操作命令を C 言語から呼び出す (Windows, Visual C++)

x64 CPU には、64 ビットレジスタ内の ビット 1 の個数を数えたり、右端あるいは左端から最初にビット 1 が現れる位置を探したり、指定した位置のビットの値を一か所に集めたりする、高機能なビット操作命令があります。それらの命令を Visual C++ から呼び出してみます。C++ ではなく C 言語を使います。

動作確認環境

  • Windows 11 Home 23H2
  • Visual Studio Community 2022 (Visual C++)
  • Intel Core i5-8265U

ビット 1 の個数を数える

次の関数で、64 ビット整数内のビット 1 の個数を数えることができます。

#include <intrin.h>
unsigned __int64 __popcnt64(unsigned __int64);

たとえば、16 進数の「1234’0000’0000’CDEF」(見やすくするためシングルクォーテーションで区切っています)を渡すと、この値は 2 進数で「0001’0010’0011’0100’0000’0000’0000’0000’0000’0000’0000’0000’1100’1101’1110’1111」であり、ビット 1 が 17 個あるので、17 が返ります。

いくつかの例を図で示します。

サンプルプログラムです。

#include <stdio.h>
#include <intrin.h>

int main()
{
    unsigned __int64 ret;
    unsigned __int64 val;

    val = 0x1234'0000'0000'CDEF;
    ret = __popcnt64(val);
    printf("%llu\n", ret);

    return 0;
}

※ 最近の Visual C++ では、「0x123400000000CDEF」に区切り記号を入れて「0x1234’0000’0000’CDEF」と書けます。

ビルドします。「/Od」は最適化抑止(デフォルト)、「/FAsc」はアセンブリリスト出力の意味です。

C:\tp\bit_operation> cl /Od /FAsc popcnt.c
Microsoft(R) C/C++ Optimizing Compiler Version 19.31.31105 for x64
Copyright (C) Microsoft Corporation.  All rights reserved.

popcnt.c
Microsoft (R) Incremental Linker Version 14.31.31105.0
Copyright (C) Microsoft Corporation.  All rights reserved.

/out:popcnt.exe
popcnt.obj

実行します。

C:\tp\bit_operation> popcnt.exe
17

「0x1234’0000’0000’CDEF」に含まれるビット 1 の個数「17」が表示されました。

アセンブリリスト (popcnt.cod) を見ると、C 言語の __popcnt64 関数が x64 の popcnt 命令 (Return the Count of Number of Bits Set to 1) に変換されていることが確認できます。

; 10   : ret = __popcnt64(val);

  00013 f3 48 0f b8 44
        24 20            popcnt  rax, QWORD PTR val$[rsp]
  0001a 48 89 44 24 28   mov     QWORD PTR ret$[rsp], rax

同様の機能を持つ型違いの関数を以下に示します。

unsigned int __popcnt(unsigned int);
unsigned short __popcnt16(unsigned short);
int _mm_popcnt_u32(unsigned int);
__int64 _mm_popcnt_u64(unsigned __int64);

右端から、連続するビット 0 の個数を数える

次の関数で、64 ビット整数の右端 (LSB, 最下位) からビット 0 が何個連続しているかを取得できます。

#include <intrin.h>
unsigned __int64 _tzcnt_u64(unsigned __int64);

たとえば、16 進数の「7000’8000’9000’A000」を渡すと、この値は 2 進数で「0111’0000’0000’0000’1000’0000’0000’0000’1001’0000’0000’0000’1010’0000’0000’0000」であり、右端からビット 0 が 13 個連続しているので、13 が返ります。

いくつかの例を図で示します。

サンプルプログラムです。

#include <stdio.h>
#include <intrin.h>

int main()
{
    unsigned __int64 ret;
    unsigned __int64 val;

    val = 0x7000'8000'9000'A000;
    ret = _tzcnt_u64(val);
    printf("%llu\n", ret);

    return 0;
}

ビルドします。

C:\tp\bit_operation> cl /Od /FAsc tzcnt.c

実行します。

C:\tp\bit_operation> tzcnt.exe
13

「0x7000’8000’9000’A000」の右端からの連続するビット 0 の個数である「13」が表示されました。

アセンブリリスト (tzcnt.cod) を見ると、C 言語の _tzcnt_u64 関数が x64 の tzcnt 命令 (Count the Number of Trailing Zero Bits) に変換されていることが確認できます。

; 10   : ret = _tzcnt_u64(val);

  00013 f3 48 0f bc 44
        24 20            tzcnt   rax, QWORD PTR val$[rsp]
  0001a 48 89 44 24 28   mov     QWORD PTR ret$[rsp], rax

同様の機能を持つ型違いの関数を以下に示します。

unsigned int _tzcnt_u32(unsigned int);

左端から、連続するビット 0 の個数を数える

次の関数で、64 ビット整数の左端 (MSB, 最上位) からビット 0 が何個連続しているかを取得できます。

#include <intrin.h>
unsigned __int64 _lzcnt_u64(unsigned __int64);

たとえば、16 進数の「0000’0500’0A00’0F00」を渡すと、この値は 2 進数で「0000’0000’0000’0000’0000’0101’0000’0000’0000’1010’0000’0000’0000’1111’0000’0000」であり、左端からビット 0 が 21 個連続しているので、21 が返ります。

いくつかの例を図で示します。

サンプルプログラムです。

#include <stdio.h>
#include <intrin.h>

int main()
{
    unsigned __int64 ret;
    unsigned __int64 val;

    val = 0x0000'0500'0A00'0F00;
    ret = _lzcnt_u64(val);
    printf("%llu\n", ret);

    return 0;
}

ビルドします。

C:\tp\bit_operation> cl /Od /FAsc lzcnt.c

実行します。

C:\tp\bit_operation> lzcnt.exe
21

「0x0000’0500’0A00’0F00」の左端からの連続するビット 0 の個数である「21」が表示されました。

アセンブリリスト (lzcnt.cod) を見ると、C 言語の _lzcnt_u64 関数が x64 の lzcnt 命令 (Count the Number of Leading Zero Bits) に変換されていることが確認できます。

; 10   : ret = _lzcnt_u64(val);

  00013 f3 48 0f bd 44
        24 20            lzcnt   rax, QWORD PTR val$[rsp]
  0001a 48 89 44 24 28   mov     QWORD PTR ret$[rsp], rax

同様の機能を持つ型違いの関数を以下に示します。

unsigned short __lzcnt16(unsigned short);
unsigned int _lzcnt_u32(unsigned int);
unsigned short __lzcnt16(unsigned short);
unsigned int __lzcnt(unsigned int);
unsigned __int64 __lzcnt64(unsigned __int64);

一番右側のビット 1 の位置を取得

前述の _tzcnt_u64 によく似た _BitScanForward64 という関数があります。

#include <intrin.h>
unsigned char _BitScanForward64(unsigned long * _Index, unsigned __int64 _Mask);

この関数は、64 ビット整数 _Mask について右端 (LSB, 最下位) から順にビット 1 を探し、見つかった場合はその位置(左端が 63、右端が 0)を _Index が指すアドレスにセットし、関数として 1 を返します。
ビット 1 が見つからなかった場合は、関数として 0 を返します。このとき、_Index が指すアドレスの内容は未定義です。

たとえば、16 進数の「7000’8000’9000’A000」を渡すと、この値は 2 進数で「0111’0000’0000’0000’1000’0000’0000’0000’1001’0000’0000’0000’1010’0000’0000’0000」であり、右端から 14 個目(位置で言うとビット 13)に 1 が見つかるので、_Index が指すアドレスに 13 がセットされ、関数として 1 が返ります。

いくつかの例を図で示します。

サンプルプログラムです。

#include <stdio.h>
#include <intrin.h>

int main()
{
    unsigned char ret;
    unsigned __int64 val;
    unsigned long index;

    val = 0x7000'8000'9000'A000;
    ret = _BitScanForward64(&index, val);
    printf("index=%lu, ret=%u\n", index, ret);

    return 0;
}

ビルドします。

C:\tp\bit_operation> cl /Od /FAsc bsf.c

実行します。

C:\tp\bit_operation> bsf.exe
index=13, ret=1

「0x7000’8000’9000’A000」の一番右側にあるビット 1 の位置「13」と、ビット 1 が見つかったことを示す「1」が表示されました。

アセンブリリスト (bsf.cod) を見ると、C 言語の _BitScanForward64 関数が x64 の bsf 命令 (Bit Scan Forward) に変換されていることが確認できます。

; 11   : ret = _BitScanForward64(&index, val);

  00013 48 8b 44 24 28   mov     rax, QWORD PTR val$[rsp]
  00018 48 0f bc c0      bsf     rax, rax
  0001c 89 44 24 24      mov     DWORD PTR index$[rsp], eax
  00020 0f 95 c0         setne   al
  00023 88 44 24 20      mov     BYTE PTR ret$[rsp], al

同様の機能を持つ型違いの関数を以下に示します。

unsigned char _BitScanForward(unsigned long * _Index, unsigned long _Mask);

一番左側のビット 1 の位置を取得

前述の _lzcnt_u64 によく似た _BitScanReverse64 という関数があります。

#include <intrin.h>
unsigned char _BitScanReverse64(unsigned long * _Index, unsigned __int64 _Mask);

この関数は、64 ビット整数 _Mask について左端 (MSB, 最上位) から順にビット 1 を探し、見つかった場合はその位置(左端が 63、右端が 0)を _Index が指すアドレスにセットし、関数として 1 を返します。
ビット 1 が見つからなかった場合は、_Index が指すアドレスの内容は未定義で、関数として 0 を返します。

たとえば、16 進数の「0000’0500’0A00’0F00」を渡すと、この値は 2 進数で「0000’0000’0000’0000’0000’0101’0000’0000’0000’1010’0000’0000’0000’1111’0000’0000」であり、左端から 22 個目(位置でいうとビット 42)に 1 が見つかるので、_Index が指すアドレスに 42 がセットされ、関数として 1 が返ります。

いくつかの例を図で示します。

サンプルプログラムです。

#include <stdio.h>
#include <intrin.h>

int main()
{
    unsigned char ret;
    unsigned __int64 val;
    unsigned long index;

    val = 0x0000'0500'0A00'0F00;
    ret = _BitScanReverse64(&index, val);
    printf("index=%lu, ret=%u\n", index, ret);

    return 0;
}

ビルドします。

C:\tp\bit_operation> cl /Od /FAsc bsr.c

実行します。

C:\tp\bit_operation> bsr.exe
index=42, ret=1

「0x0000’0500’0A00’0F00」の一番左側にあるビット 1 の位置「42」と、ビット 1 が見つかったことを示す「1」が表示されました。

アセンブリリスト (bsr.cod) を見ると、C 言語の _BitScanReverse64 関数が x64 の bsr 命令 (Bit Scan Reverse) に変換されていることが確認できます。

; 11   : ret = _BitScanReverse64(&index, val);

  00013 48 8b 44 24 28   mov     rax, QWORD PTR val$[rsp]
  00018 48 0f bd c0      bsr     rax, rax
  0001c 89 44 24 24      mov     DWORD PTR index$[rsp], eax
  00020 0f 95 c0         setne   al
  00023 88 44 24 20      mov     BYTE PTR ret$[rsp], al

同様の機能を持つ型違いの関数を以下に示します。

unsigned char _BitScanReverse(unsigned long * _Index, unsigned long _Mask);

一番右側のビット 1 のみを 1 に

次の関数は、64 ビット整数の一番右側(LSB 側, 最下位側)にあるビット 1 を残し、それ以外をすべて 0 にした値を返します。

#include <intrin.h>
unsigned __int64 _blsi_u64(unsigned __int64);

たとえば、16 進数の「7000’8000’9000’A000」、2 進数で「0111’0000’0000’0000’1000’0000’0000’0000’1001’0000’0000’0000’1010’0000’0000’0000」を渡すと、一番右側にあるビット 1 のみを残した値、すなわち 2 進数で「0000’0000’0000’0000’0000’0000’0000’0000’0000’0000’0000’0000’0010’0000’0000’0000」、16 進数で「0000’0000’0000’2000」が返ります。
0 を渡すと、0 が返ります。

いくつかの例を図で示します。

サンプルプログラムです。

#include <stdio.h>
#include <intrin.h>

int main()
{
    unsigned __int64 ret;
    unsigned __int64 val;

    val = 0x7000'8000'9000'A000;
    ret = _blsi_u64(val);
    printf("0x%016llx\n", ret);

    return 0;
}

ビルドします。

C:\tp\bit_operation> cl /Od /FAsc blsi.c

実行します。

C:\tp\bit_operation> blsi.exe
0x0000000000002000

「0x7000’8000’9000’A000」の一番右側にあるビット 1 のみを残した「0x0000000000002000」が表示されました。

アセンブリリスト (blsi.cod) を見ると、C 言語の _blsi_u64 関数が x64 の blsi 命令 (Extract Lowest Set Isolated Bit) に変換されていることが確認できます。

; 10   : ret = _blsi_u64(val);

  00013 c4 e2 f8 f3 5c
        24 20            blsi    rax, QWORD PTR val$[rsp]
  0001a 48 89 44 24 28   mov     QWORD PTR ret$[rsp], rax

同様の機能を持つ型違いの関数を以下に示します。

unsigned int _blsi_u32(unsigned int);

一番右側のビット 1 を 0 に

次の関数は、64 ビット整数の一番右側(LSB 側, 最下位側)にあるビット 1 を 0 にした値を返します。

#include <intrin.h>
unsigned __int64 _blsr_u64(unsigned __int64);

たとえば、16 進数の「7000’8000’9000’A000」、2 進数で「0111’0000’0000’0000’1000’0000’0000’0000’1001’0000’0000’0000’1010’0000’0000’0000」を渡すと、一番右側にあるビット 1 を 0 にした値、すなわち 2 進数で「0111’0000’0000’0000’1000’0000’0000’0000’1001’0000’0000’0000’1000’0000’0000’0000」、16 進数で「7000’8000’9000’8000」が返ります。
0 を渡すと、0 が返ります。

いくつかの例を図で示します。

サンプルプログラムです。

#include <stdio.h>
#include <intrin.h>

int main()
{
    unsigned __int64 ret;
    unsigned __int64 val;

    val = 0x7000'8000'9000'A000;
    ret = _blsr_u64(val);
    printf("0x%016llx\n", ret);

    return 0;
}

ビルドします。

C:\tp\bit_operation> cl /Od /FAsc blsr.c

実行します。

C:\tp\bit_operation> blsr.exe
0x7000800090008000

「0x7000’8000’9000’A000」の一番右側にあるビット 1 を 0 に変えた「0x7000800090008000」が表示されました。

アセンブリリスト (blsr.cod) を見ると、C 言語の _blsr_u64 関数が x64 の blsr 命令 (Reset Lowest Set Bit) に変換されていることが確認できます。

; 10   : ret = _blsr_u64(val);

  00013 c4 e2 f8 f3 4c
        24 20            blsr    rax, QWORD PTR val$[rsp]
  0001a 48 89 44 24 28   mov     QWORD PTR ret$[rsp], rax

同様の機能を持つ型違いの関数を以下に示します。

unsigned int _blsr_u32(unsigned int);

指定した位置とその左側すべてを 0 に

次の関数は、64 ビット整数に対して、指定した位置(左端が 63、右端が 0)とその左側(MSB 側, 最上位側)のビットすべてを 0 にした値を返します。

#include <intrin.h>
unsigned __int64 _bzhi_u64(unsigned __int64 src, unsigned int index);

たとえば、引数 src に 16 進数の「7000’8000’9000’A000」、2 進数で「0111’0000’0000’0000’1000’0000’0000’0000’1001’0000’0000’0000’1010’0000’0000’0000」を、引数 index に位置「21」を渡すと、その位置と左側すべてを 0 にした値、すなわち 2 進数で「0000’0000’0000’0000’0000’0000’0000’0000’0000’0000’0000’0000’1010’0000’0000’0000」、16 進数で「0000’0000’0000’A000」が返ります。

いくつかの例を図で示します。

サンプルプログラムです。

#include <stdio.h>
#include <intrin.h>

int main()
{
    unsigned __int64 ret;
    unsigned __int64 val;
    unsigned int index;

    val = 0x7000'8000'9000'A000;
    index = 21;
    ret = _bzhi_u64(val, index);
    printf("0x%016llx\n", ret);

    return 0;
}

ビルドします。

C:\tp\bit_operation> cl /Od /FAsc bzhi.c

実行します。

C:\tp\bit_operation> bzhi.exe
0x000000000000a000

「0x7000’8000’9000’A000」のビット位置「21」とその左側をすべて 0 にした「0x000000000000a000」が表示されました。

アセンブリリスト (bzhi.cod) を見ると、C 言語の _bzhi_u64 関数が x64 の bzhi 命令 (Zero High Bits Starting with Specified Bit Position) に変換されていることが確認できます。

; 12   : ret = _bzhi_u64(val, index);

  0001b 8b 44 24 20      mov     eax, DWORD PTR index$[rsp]
  0001f c4 e2 f8 f5 44
        24 28            bzhi    rax, QWORD PTR val$[rsp], rax
  00026 48 89 44 24 30   mov     QWORD PTR ret$[rsp], rax

同様の機能を持つ型違いの関数を以下に示します。

unsigned int _bzhi_u32(unsigned int src, unsigned int index);

右端から一番右側のビット 1 までのみを 1 に

次の関数は、64 ビット整数に対して、右端 (LSB, 最下位) から一番右側にあるビット 1 までをすべて 1 に、その左側をすべて 0 にした値を返します。

#include <intrin.h>
unsigned __int64 _blsmsk_u64(unsigned __int64);

たとえば、16 進数の「7700’9900’AA00’0000」、2 進数で「0111’0111’0000’0000’1001’1001’0000’0000’1010’1010’0000’0000’0000’0000’0000’0000」を渡すと、右端から一番右側にあるビット 1 までをすべて 1 に、その左側をすべて 0 にした値、2 進数で「0000’0000’0000’0000’0000’0000’0000’0000’0000’0011’1111’1111’1111’1111’1111’1111」、16 進数で「0000’0000’03FF’FFFF」が返ります。
0 を渡すと、16 進数で「FFFF’FFFF’FFFF’FFFF」が返ります。

いくつかの例を図で示します。

サンプルプログラムです。

#include <stdio.h>
#include <intrin.h>

int main()
{
    unsigned __int64 ret;
    unsigned __int64 val;

    val = 0x7700'9900'AA00'0000;
    ret = _blsmsk_u64(val);
    printf("0x%016llx\n", ret);

    return 0;
}

ビルドします。

C:\tp\bit_operation> cl /Od /FAsc blsmsk.c

実行します。

C:\tp\bit_operation> blsmsk.exe
0x0000000003ffffff

「0x7700’9900’AA00’0000」の右端から一番右側のビット 1 までのみを 1 にした「0x0000000003ffffff」が表示されました。

アセンブリリスト (blsmsk.cod) を見ると、C 言語の _blsmsk_u64 関数が x64 の blsmsk 命令 (Get Mask Up to Lowest Set Bit) に変換されていることが確認できます。

; 10   : ret = _blsmsk_u64(val);

  00013 c4 e2 f8 f3 54
        24 20            blsmsk  rax, QWORD PTR val$[rsp]
  0001a 48 89 44 24 28   mov     QWORD PTR ret$[rsp], rax

同様の機能を持つ型違いの関数を以下に示します。

unsigned int _blsmsk_u32(unsigned int);

バイト単位で左右反転

次の関数を使って、8 バイト(64 ビット)の値の左右をバイト単位(8 ビット単位)で反転(逆転, スワップ)できます。いわゆるエンディアンの変換になります。

#include <intrin.h>
unsigned __int64 _byteswap_uint64(unsigned __int64 val);

たとえば、16 進数の「1234’5678’9ABC’DEF0」を渡すと、バイト単位で左右を反転した「F0DE’BC9A’7856’3412」が返ります。

動作を図で示します。

サンプルプログラムです。

#include <stdio.h>
#include <intrin.h>

int main()
{
    unsigned __int64 ret;
    volatile unsigned __int64 val;

    val = 0x1234'5678'9ABC'DEF0;
    ret = _byteswap_uint64(val);
    printf("0x%016llx\n", ret);

    return 0;
}

ビルドします。(「/Od」(最適化抑止)でビルドすると _byteswap_uint64 関数がライブラリ関数の呼び出しにコンパイルされたため、「/O2」(速度優先で最大限の最適化)を指定しています。また、「/O2」でビルドすると左右反転の計算結果が定数として埋め込まれてしまったため、ソースの val 変数に volatile を付加しています。)

C:\tp\bit_operation> cl /O2 /FAsc bswap.c

実行します。

C:\tp\bit_operation> bswap
0xf0debc9a78563412

「0x1234’5678’9ABC’DEF0」をバイト単位で左右反転させた「0xf0debc9a78563412」が表示されました。

アセンブリリスト (bswap.cod) を見ると、C 言語の _byteswap_uint64 関数が x64 の bswap 命令 (Byte Swap) に変換されたことが確認できます(最適化の影響でソースの位置がずれています)。

; 10   : ret = _byteswap_uint64(val);

  00015 48 8b 54 24 30   mov     rdx, QWORD PTR val$[rsp]

; 11   :  printf("0x%016llx\n", ret);

  0001a 48 8d 0d 00 00
        00 00            lea     rcx, OFFSET FLAT:??_C@_0L@EFCPBNDB@0x?$CF016llx?6@
  00021 48 0f ca         bswap   rdx
  00024 e8 00 00 00 00   call    printf

同様の機能を持つ型違いの関数を以下に示します。

unsigned short _byteswap_ushort(unsigned short val);
unsigned long _byteswap_ulong(unsigned long val);

指定したビット位置の値を集める

次の関数を使って、64 ビット整数の指定したビット位置の値を一か所に集めることができます。

#include <intrin.h>
unsigned __int64 _pext_u64(unsigned __int64 src, unsigned __int64 mask);

たとえば、引数 src に 16 進数の「FF00’7700’4000’3020」を、引数 mask に「A000’8000’5040’3020」を渡すと、引数 src から、引数 mask で指定したビット位置の値が取り出されて右側に集約され、結果として「0000’0000’0000’01A7」が返ります。
言葉で説明しづらいので、動作を図で示します。

サンプルプログラムです。

#include <stdio.h>
#include <intrin.h>

int main()
{
    unsigned __int64 ret;
    unsigned __int64 src;
    unsigned __int64 mask;

    src = 0xFF00'7700'4000'3020;
    mask = 0xA000'8000'5040'3020;
    ret = _pext_u64(src, mask);
    printf("0x%016llx\n", ret);

    return 0;
}

ビルドします。

C:\tp\bit_operation> cl /Od /FAsc pext.c

実行します。

C:\tp\bit_operation> pext.exe
0x00000000000001a7

前述のとおり、「0x00000000000001a7」が表示されました。

アセンブリリスト (pext.cod) を見ると、C 言語の _pext_u64 関数が x64 の pext 命令 (Parallel Bits Extract) に変換されていることが確認できます。

; 12   : ret = _pext_u64(src, mask);

  00022 48 8b 44 24 20   mov     rax, QWORD PTR src$[rsp]
  00027 c4 e2 fa f5 44
        24 28            pext    rax, rax, QWORD PTR mask$[rsp]
  0002e 48 89 44 24 30   mov     QWORD PTR ret$[rsp], rax

同様の機能を持つ型違いの関数を以下に示します。

unsigned int _pext_u32(unsigned int src, unsigned int mask);

指定したビット位置に分配する

_pext_u64 関数は指定のビット位置の値を集約しましたが、逆に、指定のビット位置に値を分配するのが _pdep_u64 関数です。

#include <intrin.h>
unsigned __int64 _pdep_u64(unsigned __int64 src, unsigned __int64 mask);

たとえば、引数 src に 16 進数の「F000’E000’D000’CBA7」を、引数 mask に「A000’8000’5040’3020」を渡すと、引数 src の右側から順に、各ビットの値が引数 mask で指定したビット位置に分配され、結果として「A000’0000’4000’3020」が返ります。
言葉で説明しづらいので、動作を図で示します。

サンプルプログラムです。

#include <stdio.h>
#include <intrin.h>

int main()
{
    unsigned __int64 ret;
    unsigned __int64 src;
    unsigned __int64 mask;

    src = 0xF000'E000'D000'CBA7;
    mask = 0xA000'8000'5040'3020;
    ret = _pdep_u64(src, mask);
    printf("0x%016llx\n", ret);

    return 0;
}

ビルドします。

C:\tp\bit_operation> cl /Od /FAsc pdep.c

実行します。

C:\tp\bit_operation> pdep.exe
0xa000000040003020

前述のとおり、「0xa000000040003020」が表示されました。

アセンブリリスト (pdep.cod) を見ると、C 言語の _pdep_u64 関数が x64 の pdep 命令 (Parallel Bits Deposit) に変換されていることが確認できます。

; 12   : ret = _pdep_u64(src, mask);

  00022 48 8b 44 24 20   mov     rax, QWORD PTR src$[rsp]
  00027 c4 e2 fb f5 44
        24 28            pdep    rax, rax, QWORD PTR mask$[rsp]
  0002e 48 89 44 24 30   mov     QWORD PTR ret$[rsp], rax

同様の機能を持つ型違いの関数を以下に示します。

unsigned int _pdep_u32(unsigned int src, unsigned int mask);

ビット単位で左右反転

ビット単位で左右反転させる関数はないため、いくつかの演算を組み合わせて自分で作る必要があります。

たとえば次のプログラムでは、_byteswap_uint64 関数を使ってバイト単位で左右反転させた後、定石 [2] を使って各バイト内のビットを左右反転させ、全体として 64 ビットの左右反転を実現しています。

#include <stdio.h>
#include <intrin.h>

int main()
{
    unsigned __int64 ret;
    volatile unsigned __int64 val;

    val = 0x1234'5678'9ABC'DEF0;
    ret = _byteswap_uint64(val);
    ret = (ret & 0x0F0F0F0F0F0F0F0F) << 4 | (ret >> 4) & 0x0F0F0F0F0F0F0F0F;
    ret = (ret & 0x3333333333333333) << 2 | (ret >> 2) & 0x3333333333333333;
    ret = (ret & 0x5555555555555555) << 1 | (ret >> 1) & 0x5555555555555555;
    printf("0x%016llx\n", ret);

    return 0;
}

動作を図で示します。

ビルドします。

C:\tp\bit_operation> cl /O2 /FAsc bitswap.c

実行します。

C:\tp\bit_operation> bitswap.exe
0x0f7b3d591e6a2c48

「0x1234’5678’9ABC’DEF0」をビット単位で左右反転させた「0x0f7b3d591e6a2c48」が表示されました。

次のプログラムでは、_pext_u64 関数を使って各バイトの同じビット位置の値を集約しながらビット順を反転させ、その後、_pext_u64 関数を使って散らばった各バイトのビット値を再集約しながらバイト順を反転させ、全体として 64 ビットの左右反転を実現しています。

#include <stdio.h>
#include <intrin.h>

int main()
{
    unsigned __int64 ret;
    volatile unsigned __int64 val;
    unsigned __int64 mask;
    signed __int64 bitExt;

    val = 0x1234'5678'9ABC'DEF0;

    bitExt = 0;
    mask = 0x0101010101010101;
    for (int i = 0; i < 8; i++)
    {
        bitExt = (bitExt << 8) | _pext_u64(val, mask);
        mask <<= 1;
    }

    ret = 0;
    mask = 0x0101010101010101;
    for (int i = 0; i < 8; i++)
    {
        ret = (ret << 8) | _pext_u64(bitExt, mask);
        mask <<= 1;
    }

    printf("0x%016llx\n", ret);

    return 0;
}

動作の概略図です(大幅に省略しています)。

ビルドします。

C:\tp\bit_operation> cl /O2 /FAsc bitswap2.c

実行します。

C:\tp\bit_operation> bitswap2.exe
0x0f7b3d591e6a2c48

「0x1234’5678’9ABC’DEF0」をビット単位で左右反転させた「0x0f7b3d591e6a2c48」が表示されました。なお、このプログラムよりひとつ前の bitswap.c のほうが高速でした。

おわりに

x64 の高機能なビット操作命令を、Visual C++ 上の C 言語から使ってみました。
最後に見出しを列挙します。

  • ビット 1 の個数を数える
  • 右端から、連続するビット 0 の個数を数える
  • 左端から、連続するビット 0 の個数を数える
  • 一番右側のビット 1 の位置を取得
  • 一番左側のビット 1 の位置を取得
  • 一番右側のビット 1 のみを 1 に
  • 一番右側のビット 1 を 0 に
  • 指定した位置とその左側すべてを 0 に
  • 右端から一番右側のビット 1 までのみを 1 に
  • バイト単位で左右反転
  • 指定したビット位置の値を集める
  • 指定したビット位置に分配する
  • ビット単位で左右反転(複数命令)

初期の 8086 に比べ、ずいぶん高機能になったものです。

参考文献

[1] Intel「Intel(R) 64 and IA-32 Architectures Software Developer’s Manual」, 2023

[2] ヘンリー・S・ウォーレン、ジュニア著, 滝沢、鈴木、赤池、葛、藤波、玉井訳「ハッカーのたのしみ 本物のプログラマはいかにして問題を解くか」エスアイビーアクセス, 2004