首先:我知道大多数优化错误是由于编程错误或依赖于可能会根据优化设置(浮点值、多线程问题......)而改变的事实。
但是,我遇到了一个很难找到的错误,并且有点不确定是否有任何方法可以在不关闭优化的情况下防止此类错误的发生。我错过了什么吗?这真的是一个优化器错误吗?这是一个简化的示例:
struct Data {
int a;
int b;
double c;
};
struct Test {
void optimizeMe();
Data m_data;
};
void Test::optimizeMe() {
Data * pData; // Note that this pointer is not initialized!
bool first = true;
for (int i = 0; i < 3; ++i) {
if (first) {
first = false;
pData = &m_data;
pData->a = i * 10;
pData->b = i * pData->a;
pData->c = pData->b / 2;
} else {
pData->a = ++i;
} // end if
} // end for
};
int main(int argc, char *argv[]) {
Test test;
test.optimizeMe();
return 0;
}
当然,真正的程序要做的远不止这些。但这一切都归结为这样一个事实,即不是直接访问 m_data ,而是使用了一个(以前未初始化的)指针。一旦我向if (first)
-part 添加了足够的语句,优化器似乎就会将代码更改为以下几行:
if (first) {
first = false;
// pData-assignment has been removed!
m_data.a = i * 10;
m_data.b = i * m_data.a;
m_data.c = m_data.b / m_data.a;
} else {
pData->a = ++i; // This will crash - pData is not set yet.
} // end if
如您所见,它用直接写入成员结构替换了不必要的指针取消引用。但是,它不会在else
-branch 中执行此操作。它还删除了pData
-assignment。由于指针现在仍然未初始化,程序将在 -branch 中崩溃else
。
当然这里有很多地方可以改进,所以你可能会把它归咎于程序员:
- 忘记指针,做优化器所做的事情——
m_data
直接使用。 - 将 pData 初始化为 nullptr - 这样优化器就知道
else
如果从未分配指针,则 -branch 将失败。至少它似乎解决了我的测试环境中的问题。 - 将指针分配移到循环前面(有效地
pData
用初始化&m_data
,然后它也可以是引用而不是指针(为了很好的衡量标准)。这是有道理的,因为在所有情况下都需要 pData ,因此没有理由在内部执行此操作循环。
至少可以说,代码显然很臭,我并不是要“责怪”优化器这样做。但我在问:我做错了什么?该程序可能很丑陋,但它是有效的代码......
我应该补充一点,我正在使用带有 C++/CLI 和 v110_xp-Toolset 的 VS2012。优化设置为 /O2。另请注意,如果您真的想重现问题(但这并不是这个问题的真正重点),您需要考虑程序的复杂性。这是一个非常简化的示例,优化器有时不会删除指针分配。隐藏&m_data
在函数后面似乎是“帮助”。
编辑:
问:我如何知道编译器正在将其优化为提供的示例?
A:我不太擅长阅读汇编程序,但是我看过它并做了 3 个观察,这让我相信它的行为方式是这样的:
- 一旦优化开始(添加更多的赋值通常就可以了),指针赋值就没有关联的汇编语句。它也没有被提升到声明中,所以它看起来真的没有初始化(至少对我来说)。
- 在程序崩溃的情况下,调试器会跳过赋值语句。在程序运行没有问题的情况下,调试器会停在那里。
- 如果我在调试时观察
pData
和 的内容m_data
,它清楚地表明if
-branch 中的所有分配都影响m_data
并m_data
接收到正确的值。指针本身仍然指向它从一开始就具有的相同的未初始化值。因此我不得不假设它实际上根本没有使用指针来进行分配。
问:它与 i (循环展开)有什么关系吗?
答:不,实际程序实际上使用 do { ... } while() 来循环 SQL SELECT-resultset,因此迭代计数完全是运行时特定的,并且无法由编译器预先确定。