我正在研究一个使用以下编码的代码库,该代码指示替换样本:我们维护一个整数数组,以指示样本中和的位置,其中正整数表示位置在另一个数组中,负整数表示我们不应在此迭代中使用数据点。
示例:
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条件?
此外,因为我是这样的新手,所以为什么/如何取一个有符号正整数的位补给我们其数学补呢? (即,带有相反符号的值)
编辑:上面的方法是错误的,(int)的补码将为-(a + 1),并取决于答案中指出的int表示形式。因此,单纯采用现有值的正值的原始问题不适用,我们实际上需要执行逐位补码以获得原始值。
示例:
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
答案 0 :(得分:3)
我建议不要尝试摆弄东西。部分原因是您正在处理带符号的数字,并且如果您摆弄小东西,则会丢弃可移植性。部分原因是位摆弄不如可重用的函数可读。
一个简单的解决方案:
std::for_each(position.begin(), position.end(), [](int v) {
return std::abs(v);
});
为什么/如何取一个有符号正整数的位补给我们其数学补? (即,带有相反符号的值)
不是。无论如何,总的来说不是。它仅在对负数使用1的补码表示形式的系统上执行此操作,其原因仅是因为这是指定表示形式的方式。负数由正值的二进制补码表示。
这几天最常用的表示法是2的补码,这种方式无法正常工作。
答案 1 :(得分:2)
可能是第一个涉嫌乱搞黑客的消息来源: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])
性能较差的假设。您肯定应该有分析结果,证明在签入这种代码之前,位hack会很出色。
可以随意使用两者的快速基准测试(带拆卸功能)-我认为速度没有区别:
还要看看实际生成的程序集:
很明显, clang 看到您的bithack并没有留下深刻的印象-它在所有情况下都会产生有条件的移动(无分支)。 gcc 照您说的做,但是还有abs
的两个实现,其中一些实现利用了目标体系结构的注册语义。
如果您进入(自动)矢量化,那么事情就会变成even more muddy。无论如何,您都必须配置文件。
结论:只需编写std::abs
-您的编译器将为您完成所有的操作。
答案 2 :(得分:1)
使用函数表示意图。允许编译器的优化器做得比以往更好。
#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
答案 3 :(得分:1)
要回答扩展的问题:再次,首先编写显而易见的直观代码,然后检查编译器是否执行正确的操作:Look ma, no branches!
如果让它与自动矢量化一起玩起来很有趣,那么您可能就不会理解(或擅长判断)程序集,因此无论如何都必须进行概要分析。具体示例: https://godbolt.org/z/oaaOwJ。 clang 也喜欢展开自动矢量化的循环,而 gcc 更保守。无论如何,它仍然是无分支的。
可能是您的编译器比您更了解目标平台上指令调度的细节。如果您不对位魔术师感到迷惑,那么它本身会做的很好。如果仍然是代码中的热点,则可以查看是否可以手工制作更好的版本(但这可能必须在汇编中)。
答案 4 :(得分:1)
此外,因为我是这样的新手,所以为什么/如何取一个有符号正整数的位补给我们其数学补呢? (即,带有相反符号的值)
这个问题本身值得一个答案,因为每个人都会告诉你这是你的做法,但是没有人告诉你原因。
请注意,1 - 0 = 1
和1 - 1 = 0
。这意味着如果我们执行1 - b
,其中b
是单个位,则结果与b
或not b
(~b
)相反。还要注意,这种减法将永远不会产生借位,这非常重要,因为b
最多只能是1
。
还要注意,用n
位减去数字只是意味着执行n
1位减法,同时要注意借位。但是我们的特殊情况永远不会提出借口。
基本上,我们为按位非运算创建了数学定义。要翻转b
,请执行1 - b
。如果我们想翻转一个n
位数字,则对每一位都执行此操作。但是顺序进行n
的减法与减去两个n
的位数是相同的。因此,如果我们不想按位计算8位数字a
,则只需执行11111111 - a
,而对任何n
位数字也是如此。再次可行,因为从1
中减去一点将永远不会产生借位。
但是n
“ 1
”位的顺序是什么?它是值2^n - 1
。因此,将a
按位取非数字与计算2^n - 1 - a
相同。
现在,计算机内部的数字以模2^n
的形式存储。这是因为我们只有有限数量的位可用。您可能知道,如果使用8位,而您执行255 + 1
,则会得到0
。这是因为8位数字是对2^8 = 256
和255 + 1 = 256
取模的数字。 256
显然等于0
的模数256
。
但是为什么不倒退呢?按照这种逻辑0 - 1 = 255
,对不对?这确实是正确的。在数学上,-1
和255
是256
模的“全等”。 Congruent本质上意味着等于,但是它用于区分常规等式和模块化算术中的等式。
实际上,请注意0
与256
模256
的全等。 0 - 1 = 256 - 1 = 255
这样。 256
是我们的模数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
是负数a
的模数n
。因此,这里我们有负数。这称为二进制补码,几乎在每个现代处理器中都使用它,因为它是模块化算术2^n
中负数的数学定义,并且处理器内部的数字就像{{{ 1}}数学本身就是有效的。您可以添加和减去而无需执行任何其他步骤。乘法和除法确实需要“符号扩展”,但这只是这些操作的定义方式的一个怪癖,扩展符号时数字的含义不会改变。
使用这种方法当然会损失一点,因为现在您有一半的数字是正数,另一半是负数,但是您不能仅仅神奇地在处理器上增加一点,所以新的价值范围可以表示从2^n
到-2^(n-1)
(包括两端)。
或者,您可以保持数字不变,而不必在末尾添加2^(n-1) - 1
。这被称为一个人的补充。当然,这与数学上的负数并不完全相同,因此加,减,乘,除不仅要开箱即用,还需要额外的步骤来调整结果。这就是为什么二进制补码是有符号算术的事实上的标准的原因。还存在一个问题,在一个补数中,1
和0
代表相同的数量,零,而在两个补数中,负数2^n - 1
仍然正确0
(因为0
)。我认为Internet协议中将补码用作校验和,但除此之外,补码的用途非常有限。
但是请注意,“事实上的”标准意味着这就是每个人都在做的事情,但是没有规则说必须这样做,所以请务必确保检查您要构建的目标体系结构的文档确保您做对了。即使说实话,除非您正在研究某些非常具体的体系结构,否则如今在外面找到有用的补码处理器的机会几乎为零,但是比起遗憾,还是要安全得多。
答案 5 :(得分:0)
“是否有一个摆弄技巧,可以让我执行此操作而无需为值的符号设置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