【问题标题】:Bit fiddling: Negate an int, only if it's negative位摆弄:否定一个int,只有当它是负数时
【发布时间】:2019-03-30 06:50:42
【问题描述】:

我正在研究一个代码库,该代码库使用以下编码来指示带替换的采样:我们维护一个整数数组作为样本中位置存在的指标,其中正整数表示一个位置在另一个数组中,负整数表示我们不应在此迭代中使用数据点。

例子:

data_points: [...] // Objects vector of length 5 == position.size()
std::vector<int> position: [3, 4, -3, 1, -2]

这意味着data_points 中的第一个元素应该到桶 3,第二个到桶 4,第四个到桶 1。 负值表示在本次迭代中我们不会使用这些数据点,即第 3 和第 5 个数据点被标记为已排除,因为它们的位置值为负并且已使用position[i] = ~position[i] 设置。

诀窍是我们可能会多次执行此操作,但索引中数据点的位置不应该改变。所以在下一次迭代中,如果我们想要排除数据点 1,并包含数据点 5,我们可以这样做:

position[0] = ~position[0] // Bit-wise complement flips the sign on ints and subtracts 1
position[4] = ~position[4]

这会将位置向量变为

    std::vector<int> position: [-4, 4, -3, 1, 1]

现在问题是:在每一轮结束时,我想将所有符号重置为正数,即位置应该变为 [3, 4, 3, 1, 2]。

是否有一个小技巧可以让我在没有值符号的 if 条件的情况下执行此操作?

另外,因为我不熟悉这样的位摆弄,为什么/如何采用带符号的正整数的位补码给我们它的数学补码? (即翻转符号的相同值)

编辑:以上是错误的, a (int) 的补码将给出 -(a + 1) 并取决于答案中指出的整数的表示。所以简单地取现有值的正值的原始问题不适用,我们实际上需要执行按位补码才能得到原始值

例子:

position[0] = 1
position[0] = ~position[0] // Now position[0] is -2
//  So if we did
position[0] = std::abs(position[0]) // This is wrong, position[0] is now 2!
// If we want to reset it to 1, we need to do another complement
position[0] = ~position[0] // Now position[0] is again 1

【问题讨论】:

  • 当您可以使用position[0] = -position[0] 而不是position[0] = ~position[0] 时,您想要在位级别上工作的任何特定原因?
  • 这不能回答你的问题,但你正在以一种有点复杂的方式使用负值。就个人而言,我会用一个布尔向量should_use_bucket 来表示它。然后您可以在每轮处理结束时清除布尔值。
  • 是关于性能,你想避免分支?
  • 我会选择std::abs
  • 感谢所有 cmets。我也不喜欢像这样使用位置向量,但这是一个我正在贡献的大型代码库,我正在努力将更改保持在最低限度。

标签: c++ bit-manipulation


【解决方案1】:

可能是位旋转黑客的第一个首选来源:The eponymous site

int v;           // we want to find the absolute value of v
unsigned int r;  // the result goes here 
int const mask = v >> sizeof(int) * CHAR_BIT - 1;

r = (v + mask) ^ mask;

但是,我会质疑position[i] = std::abs(position[i]) 性能更差的假设。在签入此类代码之前,您绝对应该有分析结果来证明 bit hack 的优越性。

随意使用两者的快速基准测试(带拆卸) - 我认为速度没有差异:

gcc 8.2

clang 6.0

还看一下实际生成的程序集:

https://godbolt.org/z/Ghcw_c

显然,clang 看到了你的 bithack 并且没有留下深刻印象 - 它在所有情况下都会生成条件移动(这是无分支的)。 gcc 照你说的做,但还有两个 abs 的实现在存储中,其中一些使用了目标架构的寄存器语义。

如果你进入(自动)矢量化,事情会得到even more muddy。无论如何,您都必须进行概要分析。


结论:只要写std::abs - 你的编译器会为你做所有的比特操作。

【讨论】:

  • 好点,abs 的矢量化应用程序可能具有“足够好”的性能。我不认为这是程序最热门的路径,所以我倾向于可读代码。
  • 好像我在原来的问题中有一个错误:如果我们用补码翻转值,那么取abs 将不起作用因为绝对值会改变!很抱歉造成混乱
【解决方案2】:

我建议不要尝试摆弄。部分原因是您正在处理签名数字,如果您摆弄,您会丢弃可移植性。部分原因是位摆弄不像可重用函数那样可读。

一个简单的解决方案:

std::for_each(position.begin(), position.end(), [](int v) {
    return std::abs(v);
});

为什么/如何取带符号的正整数的位补码给我们它的数学补码? (即翻转符号的相同值)

它没有。反正不是一般的。它仅在使用 1 的补码表示负数的系统上这样做,其原因仅仅是因为这是指定表示的方式。负数用正值的二进制补码表示。

现在最常用的表示是 2 的补码,但这种方式不起作用。

【讨论】:

  • 2 的补码会给出结果吗:a (int) 的补码会给出 -(a + 1)?
  • @Bar 您可以使用-a,它可移植到所有符号表示。 (~a) + 1 对于 2 的补码是正确的,但我怀疑没有理由使用它来代替 -a
  • 同意,不知道为什么原作者在他们可以使用负数时使用按位而不是在这里。可能也处理 0,如 -0 == 0,但 ~0 != 0
  • @Bar 啊,我想我开始明白了。如果要求将任何大于或等于的数字更改为某个(任何)负值,并使用相同的操作进行还原,则按位补码非常方便,除了需要注意的是依赖于 2 的补码。在 1 的补码和符号+幅度表示中,0 将反转为 -0,这在技术上是负数,但不一定是必需的。
【解决方案3】:

使用函数来表示意图。让编译器的优化器做得比以往更好。

#include <cmath>

void include_index(int& val)
{
    val = std::abs(val);
}

void exclude_index(int& val)
{
    val = -std::abs(val);
}

bool is_included(int const& val)
{
    return val > 0;
}

godbolt 的 gcc8 x86_64 编译器的示例输出(请注意,这都是位旋转,并且没有条件跳转 - 高性能计算的祸根):

include_index(int&):
  mov eax, DWORD PTR [rdi]
  sar eax, 31
  xor DWORD PTR [rdi], eax
  sub DWORD PTR [rdi], eax
  ret
exclude_index(int&):
  mov eax, DWORD PTR [rdi]
  mov edx, DWORD PTR [rdi]
  sar eax, 31
  xor edx, eax
  sub eax, edx
  mov DWORD PTR [rdi], eax
  ret
is_included(int const&):
  mov eax, DWORD PTR [rdi]
  test eax, eax
  setg al
  ret

https://godbolt.org/z/ni6DOk

【讨论】:

    【解决方案4】:

    “是否有一个小技巧可以让我在没有值符号的 if 条件的情况下执行此操作?”

    你需要保持被改变的数字的数值不是负值吗?

    如果不是,可以使用std::max 将负值设置为零

    iValue = std::max(iValue, 0); // returns zero if iValue is less than zero
    

    如果需要保留数值,而只是将负数改为正数,那么

    iValue = std::abs(iValue); // always returns a positive value of iValue
    

    【讨论】:

      【解决方案5】:

      回答扩展问题:同样,首先编写显而易见且直观的代码,然后检查您的编译器是否正确:Look ma, no branches!

      如果您让它通过自动矢量化获得乐趣,那么您可能不会理解(或不擅长判断)程序集,因此无论如何您都必须进行概要分析。具体例子: https://godbolt.org/z/oaaOwJclang 也喜欢展开自动矢量化循环,而 gcc 更保守。无论如何,它仍然是无分支的。

      您的编译器可能比您更了解目标平台上指令调度的细节。如果您不使用位魔术来掩盖您的意图,它本身就会做得很好。如果它仍然是您代码中的热点,那么您可以去看看是否可以手工制作一个更好的版本(但这可能必须在汇编中)。

      【讨论】:

      • 谢谢,我需要习惯于阅读编译器的输出来处理这样的简单事情。它很有可能会做正确的事情。
      【解决方案6】:

      另外,因为我不熟悉这样的位摆弄,为什么/如何采用带符号的正整数的位补码给我们它的数学补码? (即翻转符号的相同值)

      这个问题本身就值得回答,因为每个人都会告诉你这就是你的做法,但没有人告诉你为什么。

      请注意1 - 0 = 11 - 1 = 0。这意味着如果我们执行1 - b,其中b 是单个位,则结果与bnot b~b)相反。还要注意这个减法永远不会产生借位,这一点非常重要,因为b最多只能是1

      还请注意,用n 位减去一个数字仅意味着执行n 1 位减法,同时处理借位。但是我们的特殊情况永远不会产生借用。

      基本上,我们已经为按位非运算创建了一个数学定义。要稍微翻转b,请执行1 - b。如果我们想翻转n 位编号,请对每一位执行此操作。但是按顺序进行n 减法与减去两个n 位数相同。因此,如果我们想计算 8 位数 a 的按位非运算,我们只需执行 11111111 - a,对于任何 n 位数也是如此。这再次有效,因为从 1 中减去一点将永远不会产生借位。

      但是n1”位的顺序是什么?这是值2^n - 1。因此,按位而不是数字 a 与计算 2^n - 1 - a 相同。

      现在,计算机中的数字存储为以 2^n 为模的数字。这是因为我们只有有限数量的可用位。您可能知道,如果您使用 8 位并执行255 + 1,您将得到0。这是因为 8 位数字是以 2^8 = 256255 + 1 = 256 为模的数字。 256 显然等于 0256

      但是为什么不反过来做呢?按照这个逻辑,0 - 1 = 255,对吧?这确实是正确的。在数学上,-1255 是“全等”模 256。 Congruent 本质上意味着等于,但它用于区分模算术中的规则相等和相等。

      注意事实上0 也与256256 一致。所以0 - 1 = 256 - 1 = 255256 是我们的模数,2^8。但是如果按位不定义为2^n - 1 - a,那么我们就有~a = 2^8 - 1 - a。您会注意到我们如何在中间添加- 1。我们可以通过添加1 来删除它。

      所以我们现在有了~a + 1 = 2^n - 1 - a + 1 = 2^n - a。但是2^n - a 是负数an。所以这里我们有我们的负数。这被称为二进制补码,几乎在每一个现代处理器中都有使用,因为它是模算术模 2^n 中负数的数学定义,并且因为处理器内部的数字就像它们在模 2^n 中一样工作数学本身就可以解决。您可以在不执行任何额外步骤的情况下进行加减运算。乘法和除法确实需要“符号扩展”,但这只是这些操作如何定义的一个怪癖,扩展符号时数字的含义不会改变。

      当然,用这种方法你会损失一点,因为你现在有一半的数字是正数,另一半是负数,但你不能只是神奇地向你的处理器添加一点,所以新的值范围你可以表示的是从-2^(n-1)2^(n-1) - 1(含)。

      或者,您可以保持数字不变,而不是在末尾添加1。这被称为一个的补码。当然,这与数学负数不太一样,所以加法、减法、乘法和除法不只是开箱即用,您需要额外的步骤来调整结果。这就是为什么二进制补码是有符号算术的事实上的标准。还有一个问题是,在一个补码中,02^n - 1 都表示相同的数量,零,而在二进制补码中,负数0 仍然正确为0(因为~0 + 1 = 2^n - 1 + 1 = 2^n = 0)。我认为一个补码在 Internet 协议中用作校验和,但除此之外它的用途非常有限。

      但请注意,“事实上的”标准意味着这正是每个人都在做的事情,但没有规定必须这样做,因此请务必检查您的目标架构的文档确定你在做正确的事。老实说,现在找到有用的补码处理器的机会几乎为零,除非您正在研究一些极其特定的架构,但还是安全总比后悔好。

      【讨论】:

        猜你喜欢
        • 2015-09-09
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2022-08-12
        • 2014-05-29
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多