16進数文字列の効率的なデータ切り出し方法について


pythonにおいて、  
16進文字列に対してビット単位でデータを切り出す関数を実現したいと考えております。  
文字列ベースで、進数変換を行い、ビット切り出しをすることはできたのですが、  
処理性能やメモリ消費量で課題を感じております。

高速、かつ、低メモリで実装するにはどのような方法で処理するのがよいでしょうか。

■ＩＮＰＵＴ  
16進文字列 ："01AC"  
切り出し開始バイト：0 ※0始まり  
切り出し開始ビット：4 ※0始まり  
切り出しビット数 ：8

■ＯＵＴＰＵＴ  
切り出されたビット：00011111

■現在の処理実現方法

  1. 16進文字列を整数化 ："01AC" => 428
  2. 処理1をもとに2進文字列に変換 ："0000000110101100"
  3. 処理2に対してsliceで該当ビットの切り出し ："00011010"

■その他  
bytesを利用することを考えましたが、  
文字エンコードをしたいわけではなく、  
値としてそのまま利用したかったため、  
利用ケースにはマッチしないと判断しました。




以下、`[2^13, 2^60)` から整数をランダムに `10,000,000` 個抽出して計測してみました。実行環境は以下の通りです。

[code]

    $ grep -E '^model name' /proc/cpuinfo | uniq
    model name  : Intel(R) Core(TM) i5-8500T CPU @ 2.10GHz
    
    $ uname -isr
    Linux 6.5.0-10-generic x86_64
    
    $ lsb_release -ir
    Distributor ID: Ubuntu
    Release:    23.10
    
    $ python3 -V
    Python 3.11.6
    
[/code]

`cut_out_bits` も `cut_out_bits2` も同様の処理(文字列処理)ですが、ごく僅かに `cut_out_bits`
の方が速いです。もっとも、実行速度を重視するのであれば Python ではなく、他のプログラミング言語の方を使う方がよいかと思います。

[code]

    def cut_out_bits(hex_string, offset_bytes, offset_bits, nbits):
        bits = bin(int(hex_string, 16))[2:]
        bits = '0'*((rem:=len(bits)%8) and (8-rem)) + bits
        b = offset_bytes * 8 + offset_bits
        e = b + nbits
        return bits[b:e]
    
    def cut_out_bits_2(hex_string, offset_bytes, offset_bits, nbits):
        v = int(hex_string, 16)
        w = (v.bit_length() + 7) // 8 * 8
        bits = format(v, f'0{w}b')
        b = offset_bytes * 8 + offset_bits
        e = b + nbits
        return bits[b:e]
    
    if __name__ == '__main__':
        import random
        import timeit
        from pprint import pprint
    
        hex_strings = [hex(i)[2:] for i in random.sample(range(2**13, 2**60), k=10_000_000)]
        def apply(func, hex_strings):
            for h in hex_strings:
                func(h, 0, 4, 8)
    
        result = timeit.repeat('apply(cut_out_bits, hex_strings)', globals=globals(), number=1)
        pprint(result)
        result = timeit.repeat('apply(cut_out_bits_2, hex_strings)', globals=globals(), number=1)
        pprint(result)
    
    # cut_out_bits
    # [5.886475351173431,
    #  5.877643479034305,
    #  5.87063645105809,
    #  5.877712128916755,
    #  5.881971047958359]
    # 
    # cut_out_bits2
    # [6.823900946881622,
    #  6.91018356801942,
    #  6.978068734984845,
    #  6.980364801129326,
    #  6.996410653926432]
    
[/code]

