当前位置: 首页 > news >正文

Java19 Integer 位操作精解:compress与expand《Hacker‘s Delight》(第二版,7.4节)

compress(int i, int mask) 

这个方法是Java 19中新增的一个强大的位操作函数。

compress 方法的核心功能可以理解为 “按位过滤和压缩” 。

  1. 过滤 (Filter): 它使用 mask(掩码)作为过滤器。对于输入整数 i,只有那些在 mask 中对应位为 1 的比特才会被保留。
  2. 压缩 (Compress): 它将所有被保留下来的比特,按照它们在 i 中从低到高的原始顺序,紧凑地排列到返回值的低位端。

一个直观的例子

文档中给出的例子非常经典:compress(0xCAFEBABE, 0xFF00FFF0)

  • 输入 i0xCAFEBABE -> 1100 1010 1111 1110 1011 1010 1011 1110
  • 掩码 mask0xFF00FFF0 -> 1111 1111 0000 0000 1111 1111 1111 0000

执行过程:

  1. mask 的高8位是 FF,所以 i 的高8位 CA 被选中。
  2. mask 的次高8位是 00,所以 i 的次高8位 FE 被丢弃。
  3. mask 的次低12位是 FFF,所以 i 的对应12位 BAB 被选中。
  4. mask 的最低4位是 0,所以 i 的最低4位 E 被丢弃。
  5. 选中的比特 CABAB 被按顺序紧凑地排列,得到 0x000CABAB

expand

public static int expand(int i, int mask) 是一个静态方法,用于根据一个给定的掩码 mask 来“扩展”或“解压缩”整数 i 的位(bits)。这个操作可以看作是 Integer.compress(int i, int mask) 方法的逆过程。

核心功能: 对于 mask 中每一个为 1 的位,该方法会从输入整数 i 中按顺序(从最低位开始)取一个位,并将其放置到结果中与 mask 中那个 1 位对应的位置。结果中所有对应 mask 中 0 位的位置都将被清零。

Javadoc 注释解析与示例

我们先来看一下该方法的 Javadoc 注释,特别是它给出的示例,这有助于直观地理解其功能。

// ... existing code ...* @apiNote* Consider the simple case of expanding the digits of a hexadecimal* value:* {@snippet lang="java" :* expand(0x0000CABAB, 0xFF00FFF0) == 0xCA00BAB0* }* Starting from the least significant hexadecimal digit at position 0* from the right, the mask {@code 0xFF00FFF0} selects the first five* hexadecimal digits of {@code 0x0000CABAB}. The selected digits occur* in the resulting expanded value in order at positions 1, 2, 3, 6, and 7.
// ... existing code ...

让我们来分解这个例子:

  • 输入 i0x0000CABAB
  • 掩码 mask0xFF00FFF0
  • 预期结果0xCA00BAB0

分析过程:

  1. mask (0xFF00FFF0) 的二进制形式定义了哪些位是“有效”的目标位置。它的十六进制表示中,值为 F 的位置(第1, 2, 3, 6, 7位)代表了目标位置。
  2. i (0x0000CABAB) 是源数据。我们从 i 的最低位开始,依次取出位。
  3. expand 操作会将 i 的位填充到 mask 指定的有效位置中:
    • i 的最低的4位是 B (1011),它被放置在 mask 第一个有效区域(十六进制位 1),结果的 ...xxxB0 部分形成。
    • i 的接下来4位是 A (1010),它被放置在 mask 第二个有效区域(十六进制位 2),结果的 ...xxAB0 部分形成。
    • i 的再接下来4位是 B (1011),它被放置在 mask 第三个有效区域(十六进制位 3),结果的 ...xBABA0 部分形成。
    • mask 的第4、5位是 00,所以结果的对应位置也是 00
    • i 的再接下来4位是 A (1010),它被放置在 mask 第四个有效区域(十六进制位 6),结果的 ...A00BAB0 部分形成。
    • i 的最后4位是 C (1100),它被放置在 mask 第五个有效区域(十六进制位 7),最终形成 0xCA00BAB0

Javadoc 还提到了一个关键恒等式:expand(compress(x, m), m) == x & m。这清晰地表明 expand 和 compress 是一对互逆的操作。

源码解析:compress和expand是如何实现的?

实现源自经典著作《Hacker's Delight》(第二版,7.4节),是一种不包含分支和循环(Java代码中的for循环在编译后会展开)的高效并行算法。

以下分析改自 《Hacker's Delight》(第二版,7.4节)

compress

分步移动的数学原理​

  • ​总体目标​​:

    每个位需右移的距离 = 该位 对应mask ​​右侧 0 的数量​​(记为 d

  • ​分治策略​​:

    将 d拆解为二进制分量,分 5 轮处理:

    d = d₀ + 2×d₁ + 4×d₂ + 8×d₃ + 16×d₄ (其中 dᵢ ∈ {0,1} 是二进制系数)

    • ​第 j 轮​​:处理 ​​2ʲ​​ 的权重分量

    • ​移动条件​​:当轮需移动的位 = d二进制展开中 ​​2ʲ 的系数 dⱼ 为 1​​ 的位

​示例​​:某位需移动 d=6(二进制 110

  • 第一轮(j=0):移动 2⁰=1位(因 d₀=0→ ​​不移动​​)

  • 第二轮(j=1):移动 2¹=2位(因 d₁=1→ ​​移动​​)

  • 第三轮(j=2):移动 2²=4位(因 d₂=1→ ​​移动​​)

源码:

    @IntrinsicCandidatepublic static int compress(int i, int mask) {// See Hacker's Delight (2nd ed) section 7.4 Compress, or Generalized Extracti = i & mask; // Clear irrelevant bitsint maskCount = ~mask << 1; // Count 0's to rightfor (int j = 0; j < 5; j++) {// Parallel prefix// Mask prefix identifies bits of the mask that have an odd number of 0's to the rightint maskPrefix = parallelSuffix(maskCount);// Bits to moveint maskMove = maskPrefix & mask;// Compress maskmask = (mask ^ maskMove) | (maskMove >>> (1 << j));// Bits of i to be movedint t = i & maskMove;// Compress ii = (i ^ t) | (t >>> (1 << j));// Adjust the mask count by identifying bits that have 0 to the rightmaskCount = maskCount & ~maskPrefix;}return i;}

假设输入为:
x = abcd efgh ijkl mnop qrst uvwx yzAB CDEF,(Java代码里的 i
m = 1000 1000 1110 0000 0000 1111 0101 0101,

其中 x 中的每个字母代表一个比特(值为 0 或 1)。

x 中对应的比特 向右移动位数 等于该比特右边 m 中 0 的数量

首先清除 x 中不相关的比特会很方便,得到:
x = a000 e000 ijk0 0000 0000 uvwx 0z0B 0D0F。


首先确定哪些比特需要(向右)移动奇数个位置,并将它们移动一个比特位。​
这可以通过计算 mk = ~m << 1 并对结果执行 parallelSuffix 来完成。

得到:
mk = 1110 1110 0011 1111 1110 0001 0101 0100,
mp = 1010 0101 1110 1010 1010 0000 1100 1100。

可以观察到,

  • mk 标识了 m 中紧邻右侧是 0 的比特位,
  • mp 从右到左对这些位进行模 2 加法(parallelSuffix)。

因此,mp 标识了 m 中右侧有奇数个 0 的比特位。


​将要移动一个位置的比特是那些位于严格右侧有奇数个 0 的位置(由 mp 标识)并且在原始掩码中为 1-比特的位。​

这可以简单地通过 mv = mp & m 计算得出:
mv = 1000 0000 1110 0000 0000 0000 0100 0100。

m 的这些比特可以通过赋值语句移动:
m = (m ^ mv) | (mv >> 1);

x 的相同比特可以通过两个赋值语句移动:
t = x & mv;
x = (x ^ t) | (t >> 1);

(移动 m 的比特更简单,因为所有选中的比特都是 1。)

这里的异或操作是关闭 m 和 x 中已知的 1-比特,而或操作是打开 m 和 x 中已知的 0-比特。
这些操作也可以分别替换为异或,或者减法和加法。

​将由 mv 选择的比特向右移动一个位置后,结果是:​
m = 0100 1000 0111 0000 0000 1111 0011 0011,
x = 0a00 e000 0ijk 0000 0000 uvwx 00zB 00DF。


​现在我们必须为第二次迭代准备一个掩码,在这次迭代中,我们识别要向右移动 2 的奇数倍位置的比特。​

注意,mk & ~mp 这个量标识了那些在原始掩码 m 中紧邻右侧有偶数 0 的比特。【因为奇数0的位置 被删除了

这个量如果从右侧用 parallelSuffix 求和,就能识别出那些向右移动 2 的奇数倍(2, 6, 10 等)位置的比特。
因此,该过程就是将这个量赋给 mk ,并执行上述步骤的第二次迭代。

​修订后的 mk 值为:​
mk = 0100 1010 0001 0101 0100 0001 0001 0000。

expand

compress的逆向移动

public static int expand(int i, int mask) {// Save original maskint originalMask = mask;// Count 0's to rightint maskCount = ~mask << 1;int maskPrefix = parallelSuffix(maskCount);// Bits to moveint maskMove1 = maskPrefix & mask;// Compress maskmask = (mask ^ maskMove1) | (maskMove1 >>> (1 << 0));maskCount = maskCount & ~maskPrefix;maskPrefix = parallelSuffix(maskCount);// Bits to moveint maskMove2 = maskPrefix & mask;// Compress maskmask = (mask ^ maskMove2) | (maskMove2 >>> (1 << 1));maskCount = maskCount & ~maskPrefix;maskPrefix = parallelSuffix(maskCount);// Bits to moveint maskMove3 = maskPrefix & mask;// Compress maskmask = (mask ^ maskMove3) | (maskMove3 >>> (1 << 2));maskCount = maskCount & ~maskPrefix;maskPrefix = parallelSuffix(maskCount);// Bits to moveint maskMove4 = maskPrefix & mask;// Compress maskmask = (mask ^ maskMove4) | (maskMove4 >>> (1 << 3));maskCount = maskCount & ~maskPrefix;maskPrefix = parallelSuffix(maskCount);// Bits to moveint maskMove5 = maskPrefix & mask;int t = i << (1 << 4);i = (i & ~maskMove5) | (t & maskMove5);t = i << (1 << 3);i = (i & ~maskMove4) | (t & maskMove4);t = i << (1 << 2);i = (i & ~maskMove3) | (t & maskMove3);t = i << (1 << 1);i = (i & ~maskMove2) | (t & maskMove2);t = i << (1 << 0);i = (i & ~maskMove1) | (t & maskMove1);// Clear irrelevant bitsreturn i & originalMask;}

parallelSuffix(int maskCount)

这个方法是 Integer.compress 和 Integer.expand 的核心辅助函数。它的名字虽然叫 parallelSuffix(并行后缀),但其实现的是一种“并行前缀异或扫描”(Parallel Prefix XOR Scan)算法。

此方法计算一个“前缀和”,但使用的不是加法,而是^(按位异或)运算。对于返回结果 maskPrefix 中的任意比特位 k,它的值等于输入 maskCount 中从第 0 位到第 k 位所有比特的异或总和。

result[k] = maskCount[0] ^ maskCount[1] ^ ... ^ maskCount[k]

该算法采用分治策略,在对数时间内完成计算:

// ... existing code ...@ForceInlineprivate static int parallelSuffix(int maskCount) {// 步骤1: 计算相邻比特的异或和int maskPrefix = maskCount ^ (maskCount << 1);// 步骤2: 计算相邻2比特块的异或和maskPrefix = maskPrefix ^ (maskPrefix << 2);// 步骤3: 计算相邻4比特块的异或和maskPrefix = maskPrefix ^ (maskPrefix << 4);// 步骤4: 计算相邻8比特块的异或和maskPrefix = maskPrefix ^ (maskPrefix << 8);// 步骤5: 计算相邻16比特块的异或和maskPrefix = maskPrefix ^ (maskPrefix << 16);return maskPrefix;}
// ... existing code ...
  • maskPrefix = maskCount ^ (maskCount << 1);: 第一步,每个比特位与它右边(低位)的比特进行异或。现在每个比特位存储了它自己和右边邻居的异或结果。
  • maskPrefix = maskPrefix ^ (maskPrefix << 2);: 第二步,将相邻的2比特块进行异或。这会把之前的结果组合起来,现在每个比特位存储了原始值中连续4个比特的异或和。
  • 后续步骤: 这个过程不断重复,块的大小翻倍(4, 8, 16),直到最终每个比特位都包含了从最低位到当前位所有比特的异或总和。

在 compress 和 expand 方法中,需要将源整数中的某些位根据掩码(mask)移动到新的位置。这个移动的距离不是固定的。parallelSuffix 的作用就是高效地、并行地计算出所有需要移动的位应该移动多远,是实现这两个复杂位操作算法的关键基石。

@ForceInline 注解建议JIT编译器将这个短小精悍的函数直接内联到调用它的地方(compressexpand),以消除函数调用的开销,追求极致性能。

http://www.dtcms.com/a/328851.html

相关文章:

  • 向长波红外成像图注入非均匀噪声
  • 【嵌入式电机控制#31】FOC:霍尔安装误差的补偿
  • Unity:GUI笔记(二)——工具栏和选择网格、滚动列表和分组、窗口、自定义皮肤样式、自动布局
  • Linux系统有何特点?linux系统组成如何?
  • NTUSER.DAT是什么文件
  • 华为云之Redis部署及基础语法
  • 遨游通讯推出两款三防平板,满足“危急特”场景定制化需求
  • 《AVL树的原理与C++实现:详解平衡二叉搜索树的高效构建与操作》
  • ACL 可以限制哪些流量?入方向和出方向怎么判断?
  • 高级IO(五种IO模型介绍)
  • wordpress不同页面调用不同keywords和description
  • STM32CubeMX + HAL 库:用FSMC接口与IS62WV51216芯片实现stm32外部SRAM扩展
  • csp知识基础——贪心算法
  • Java协程深度教程:从概念到Spring Boot实战
  • XCZU6CG-2FFVC900I Xilinx FPGA AMD ZynqUltraScale+ MPSoC
  • 【计算机网络】王道考研笔记整理(4)网络层
  • 【C++】哈希的应用:位图和布隆过滤器
  • VMD+皮尔逊+降噪+重构(送报告+PPT)Matlab程序
  • Java Record 类 — 简化不可变对象的写法
  • MATLAB 绘图速查笔记
  • 模式设计:策略模式及其应用场景
  • Vue3 中 <script setup> 场景下,需要手动导入和不需要手动导入的内容整理
  • HarmonyOS Navigation路由跳转的完整示例
  • 【Qt开发】常用控件(三) -> geometry
  • 重生之我在公司写前端 | “博灵语音通知终端” | 登录页面
  • Swift 实战:从数据流到不重叠区间的高效转换
  • 《书写范式》——代码如诗,诗娟代码(Python)(附精巧“九九表”生成代码)
  • 《Linux基础知识-2》
  • 【2025】Datawhale AI夏令营-多模态RAG-Task3笔记-解决方案进阶
  • HGDB的分区表实现SQL Server的分区视图