求助:GCC 开启 O2 后程序行为不符合预期
  • 板块学术版
  • 楼主NGC5457
  • 当前回复9
  • 已保存回复9
  • 发布时间2023/1/2 16:52
  • 上次更新2023/10/24 05:48:34
查看原帖
求助:GCC 开启 O2 后程序行为不符合预期
241614
NGC5457楼主2023/1/2 16:52

如题。

该题目需要在线段树上维护区间内最大值、最大值个数、严格次大值、严格次大值个数以及区间和。合并时采用归并。我采用如下实现:

using pint = pair <int, int>; // pint { 最大值, 计数 }
using ll = long long;
#define fst first
#define scd second
struct info { pint mx[2]; ll sum; } dta[N<<2];
info merge (const info &lft, const info &rht) const {
	static pint mx[4];
	const pint *lmx = lft.mx, *rmx = rht.mx,
		*p = lmx, *q = rmx, *act; pint *pt = mx-1;
	memset (mx, 0, sizeof mx);
	while (p < lmx+2 || q < rmx+2) {
		if (q >= rmx+2 || (p < lmx+2 && p->fst >= q->fst))
			act = p++;
		else act = q++;
		if (pt >= mx && pt->fst == act->fst)
			pt->scd += act->scd;
		else *++pt = *act;
	}
	return {{ mx[0], mx[1] }, lft.sum + rht.sum };
}

我们保证,任何子节点均定义有严格次大值(不存在即设置为 -INF),故而函数主体执行完后必然有 pt >= mx + 1 为真,return 语句始终访问在上文中被赋值过的节点。从而得出,上文中 memset (mx, 0, sizeof mx) 是完全没有必要写的;但写了亦不影响最终结果

然而,在 MinGW GCC 4.8.2MinGW GCC 9.4.0 上,用编译参数 -std=c++14 -O2 -Wall-std=c++14 -Wall 分别编译后,运行输出的结果不同。我已经排除了代码中其他任何部分的可能错误;通过 -fsanitize=address/leak/undefined 和静态查错均未发现 undefined behavior 和内存泄漏我承认上文中 pint *pt = mx-1 的写法相当危险,但可以看到下文中有 pt >= mx 的检查。 有四点可以佐证:

一、容易发现下列写法是完全等价的:

info merge (const info &lft, const info &rht) const {
	static pint mx[4];
	const pint *lmx = lft.mx, *rmx = rht.mx,
		*p = lmx, *q = rmx, *act; int idx = -1;
	memset (mx, 0, sizeof mx);
	while (p < lmx+2 || q < rmx+2) {
		if (q >= rmx+2 || (p < lmx+2 && p->fst >= q->fst))
			act = p++;
		else act = q++;
		if (~idx && mx[idx].fst == act->fst)
			mx[idx] += act->scd;
		else mx[++idx] = *act;
	}
	return {{ mx[0], mx[1] }, lft.sum + rht.sum };
}

仅仅将指针换作下标而已。但这时我们开启 -O2,会发现它得到正确答案。

二、Ubuntu 20.04 上使用 clang-10 以相同参数编译第一份代码,输出正确答案。

三、我们将 memset (mx, 0, sizeof mx) 替换为

memset (mx, 0, sizeof mx);
fprintf (stderr, "%d", mx[0].fst);

我们相信它对答案没有任何影响,对吧?只是向标准错误输出了些无用的“0”。但一旦添加这句毫无关联的语句,用 gcc 编译也能得到正确的输出。

四、直接把 memset 删了,输出正确答案。

我使用 Compiler Explorer 查看二者的汇编代码,发现 memset 对应的部分在不开启 -O2 时会有传参以及 call memset 的操作;但开启 -O2 后只有 movaps。我并不理解这几句汇编的意思。

提出我的猜想:由于我使用的指针 pt 在一开始并未指向 mx 数组的任何元素,且下文中也没有明确调用 mxgcc 帮我“优化”掉了和 mx 相关的所有操作;而使用下标 idx(第二份代码),则明确地指出需要访问 mx 中元素,故而 gcc 老实地作 memset

但问题在于:

  1. 出现这种现象的根本原因是什么?
  2. 这是我在模拟赛写的代码,由于这个问题从 100 pts\text{100 pts} 变成了鸭蛋。在正式考试中怎样避免?例如,减少指针的使用?
  3. 如果真的在大赛中出现这种问题,我是否有向 CCF 申诉并拿回分数的权利?

如果您有兴趣查看完整的代码和题面,可以私信我。

2023/1/2 16:52
加载中...