XOR交换算法中运算符的未定义行为?

Posted

技术标签:

【中文标题】XOR交换算法中运算符的未定义行为?【英文标题】:Undefined behaviour of operators in XOR swap algorithm? 【发布时间】:2015-05-01 04:02:35 【问题描述】:
void swap(int* a, int* b) 
    if (a != b)
        *a ^= *b ^= *a ^= *b;

由于上述*a ^= *b ^= *a ^= *b 只是*a = *a ^ (*b = *b ^ (*a = *a ^ *b)) 的快捷方式,是否可以(例如)在第三个*a 被修改(通过=)之前评估第二个*a(用于异或)?

我用C99/C11/C++98/C++11写有关系吗?

【问题讨论】:

我记得这里有一个讨论,关于在 C11 中是否允许使用新的排序规则。在 C99 中,它显然是未定义的(*a 被修改了两次,中间没有序列点)。 我记得 C++ 对其赋值运算符做了一些额外的排序保证,这是必需的,因为 C 赋值运算符返回值,但 C++ 赋值运算符返回左值,并且后续的左值到右值的转换应该很好 -定义的行为。结果可能在 C++ 中是有效的,但我不确定。 @hvd:由于线程标准化,C11 采用了 C++ 排序模型。现在,在评估 LHS 和 RHS 之后对分配的 LHS 的修改进行排序。 我唯一使用 XOR hack 的就是宏(因为我不需要知道声明临时的类型并且可以对所有整数类型使用相同的 SWAP 宏. 如果这应该扩展为一个表达式,#define SWAP(p, q) (*(p) ^= *(q), *(q) ^= *(p), *(p) = *(q)) 对所有标准都有很好的定义,并且还具有更新的*p 的值(作为问题中的表达式)。是否有 any 使用这种情况? @mafso;在 C11 中,赋值的 LHS 的修改确实在 LHS 和 RHS 的评估之后排序,但它不能保证对 RHS 的修改在 LHS 之前排序,这与 C++11 不同。 【参考方案1】:

C++11 标准说:

5.17/1:赋值运算符 (=) 和复合赋值运算符都从右到左分组。 (...) 任务是 在左右操作数的值计算之后排序, 在赋值表达式的值计算之前。

1.9/15:如果标量对象的副作用相对于同一标量对象的另一个副作用或值而言是无序的 使用相同标量对象的值进行计算,行为是 未定义。

所以*a ^= *b 的顺序如下:

    计算 *a*b确定在哪个 订购 执行异或操作 赋值完成,即新值存储在*a 新值用作表达式(*a ^= *b) 的结果

现在*b ^= *a ^= *b,根据优先级规则是*b ^= (*a ^= *b)

    计算 *b(*a ^= *b)。它决定了哪个顺序。但由于*b 没有被(*a ^= *b) 修改,所以没关系。 执行异或操作 赋值完成,即新值存储在*b

但是现在到*a ^= *b ^= *a ^= *b未指定排序,这是根据优先级规则*a ^= (*b ^= (*a ^= *b) )

    计算 *a(*b ^= (*a ^= *b) )。它决定了哪个顺序。但正如*a 是由(*b ^= (*a ^= *b) ) 修改的。所以结果将取决于首先计算哪个值。这明明是U.B.

假设首先评估 *a,(即在其他任何事情之前): 你会得到它的原始值,它将与(*b ^= (*a ^= *b) )的值异或,即原始*b与原始*a异或再次与*b异或。这将导致 0(将存储在 *a 中)。

假设先评估(*b ^= (*a ^= *b) ),那么它的结果就是原来的*a,但是*a的内容变成原来的*a和原来的*b异或了.因此这将导致原始的*b(将存储在*a

顺便说一句,在这两种情况下,*b 都包含 *a 的原始值与 *b 异或两次,这意味着 *b 将包含原始 *a

结论: 这里证明*b 的最终值是由这个表达式唯一确定的,但*a 的最终值不是唯一定义的(可能有两个值)。所以这显然是一个未指定/未定义的结果!它可能会交换或丢失*a,具体取决于您的编译器。

如何确定交换?

我已经在上面证明了前两个复合赋值是明确指定的。 所以我们只需要确保最后一个复合赋值在它之后完成。这可以通过逗号运算符来保证:

5.18/1:一对用逗号分隔的表达式从左到右求值,左边表达式的值被丢弃

因此,以下将起作用:

void safe_swap(int* a, int* b) 
    if (a != b)
        *b ^= *a ^= *b, *a ^= *b;

编辑:但为什么要进行异或交换?

在一些没有更多可用内存的嵌入式设备上,在极端条件下可能不得不使用这种高级技巧。但它有缺点。

首先,它很难理解,并且如上所示,容易出错。那么它可能不像看起来那么有效。一些与实现相关的实验show less optimal code:3 个MOV 和 3 个XOR,而使用临时变量的经典交换只有 4 个MOV。一些 informal benchmarks 建议它在大多数情况下可能会慢 3% 到 8%。

顺便说一句,经典的swap也可以写成一个语句:

void modern_swap(int*a, int*b) 
    if (a!=b) 
        tie(*a,*b)=make_pair(*b,*a);
 

【讨论】:

如果您是正确的,*a 可能有两个值,那将不是未定义的行为,最坏的情况是未指定。在大多数版本的 C 和 C++ 标准中,行为实际上是未定义的,这不仅意味着 any 值有效,还意味着 no 值有效(程序崩溃,例如)。但是您确实提出了一个有趣的观点,即即使定义了行为,也不需要定义值。 @hvd 您对术语的看法是正确的:在 5/4 中,它说未定义的行为是“结果未在数学上定义或不在其类型的可表示值范围内”。抱歉,如果我倾向于在数学意义上使用“未定义”。我会改正的 没有“未指定/未定义的结果”之类的东西。要么是未定义的行为,要么是未指定的行为。 由于异或交换效率不高,实际使用int tmp = *a; *a=*b; *b=tmp;作为函数体会好得多。编译器知道如何使用临时寄存器,并且可以更轻松地内联和优化它。 (IMO 任何关于 xor-swap 的讨论都需要指出,你不应该在现实生活中使用它,只能作为每个人都熟悉的语言律师案例。) @PeterCordes 你当然是完全正确的!我只是解决了 OP 想要使用的表达方式,而没有质疑可以证明这种方法合理性的原因。使用 xor,它对人类的可读性较差,它是 3 mov + 3 xor,而经典版本只有 4 mov。如果目标是只使用一个语句,那么tie(*a,*b)=make_pair(*b,*a); 可以完美地完成这项工作:godbolt.org/z/LaaGlh - 关于优化的结论:永远不要自己做编译器可以为你做的事情;-)

以上是关于XOR交换算法中运算符的未定义行为?的主要内容,如果未能解决你的问题,请参考以下文章

Python 操作Redis

python爬虫入门----- 阿里巴巴供应商爬虫

Python词典设置默认值小技巧

《python学习手册(第4版)》pdf

Django settings.py 的media路径设置

Python中的赋值,浅拷贝和深拷贝的区别