如何告诉编译器不要优化掉某些代码?

3
有没有一种方法可以告诉编译器(我的是g++),即使该代码不可达,也不要优化掉某些代码?我只想在目标文件中保留这些符号。
示例:这里有一个简单的函数,即使从未调用,我也希望该函数被编译。
void foo(){
  Foo<int> v;
}

如果没有官方的编译器指令,有没有什么技巧可以让编译器认为这是一个重要的函数?或者至少让它认为它不能被安全地忽略?我尝试了类似于这样的方法:

extern bool bar;
void foo(){
  if(bar){
    Foo<int> v;
  }
}

但似乎这样做不行。

(如果你真的想知道我为什么要这样做——与这个问题有关,其中,我不想使用template class Foo<int>进行显式模板实例化,而是想写Foo<int> v,因为在许多情况下这样更容易,因为它隐式地实例化了所需的所有函数,并且在没有优化的调试模式下可以正常工作...)

更新:

以下是我想要做的事情(一个可编译的迷你示例):

foo.h(此类文件已经给出,无法更改)

template<class T>
struct Foo {
  T val_;
  Foo(T val) : val_(val) {
      // heavy code, long compile times
  }
};

foo-instantiation.cpp

#include "foo.h"
void neverCalled() {
  Foo<int> f(1);
}

// The standard way to instantiate it is this:
// template class Foo<int>;
// but in reality it is often hard to find out 
// exactly what types I have to declare.
// Usage like Foo<int> f(1); will instantiate all
// dependent types if necessary.

foo-decl.h是我从foo.h中提取的接口文件。

template<class T>
struct Foo {
  T val_;
  Foo(T val); // no heavy code, can include anywhere and compile fast
};

main.cpp

#include <iostream>
#include "foo-decl.h"

int main(int argc, char** argv){
  Foo<int> foo(1);
  return 0;
}

编译(无优化)

g++ -c main.cpp
g++ -c foo-instantiation.cpp
g++ main.o foo-instantiation.oo

编译(优化)

g++ -O2 -c main.cpp
g++ -O2 -c foo-instantiation.cpp
g++ main.o foo-instantiation.oo
main.o(.text+0x13): In function `main':
: undefined reference to `Foo<int>::Foo(int)'
collect2: ld returned 1 exit status
  • 我尝试使用预编译头文件,但是使用模板实例化方法可以使编译速度更快。
  • 不使用优化编译foo-instantiation.cpp并不理想,因为这会导致库代码(foo.h和其他)运行速度变慢。

你能否发布一些可编译的代码,以说明你的意思? - anon
7个回答

7

你遇到了One Definition Rule问题。在一个文件中,你有一个定义:

template<class T>
struct Foo {
  T val_;
  Foo(T val) : val_(val) {
      // heavy code, long compile times
  }
};

在另一个不同的定义中:
template<class T>
struct Foo {
  T val_;
  Foo(T val); // no heavy code, can include anywhere and compile fast
};

在C++中,这是明确不允许的(只允许一个相同的定义),如果你违反了规则,你的代码可能会有时候看起来好像工作正常,但实际上你会遇到可怕的“未定义行为” - 任何事情都可能发生,这取决于月相(但更可能是编译器在某些关键时刻的内部状态)。

基本上,你不能写那样的代码 - 抱歉。


1
dehmann,我仍然不明白你的做法比显式实例化好在哪里。它们都有相同的缺点,不是吗?(并且你的方法还存在未定义行为) - Johannes Schaub - litb
这不是ODR违规问题。编译器明确指出类没有定义,而不是有多个定义。 使用模板的ODR违规是一个棘手的问题,评论中没有足够的空间进行全面描述,但这不是这种情况。 - David Rodríguez - dribeas
@dribeas:实际上,如果你将foo.h更改为包含foo-decl.h并在类体外添加函数定义,它就可以正常工作。(但对我来说这不是一个选项,因为我不能更改所有类似于foo.h的文件,它们太多了,修改它们太危险了。) - Frank

4
编译器无法优化函数体,无论你是否声明为extern,因为它无法知道该函数是否被另一个编译单元调用。如果你将其声明为static,则可以进行优化,但我不相信有任何编译器会这样做。
编译器可以优化函数调用:
while(false) {
  foo();
}

在上述代码中,可以省略对foo()的调用。
另一方面,如果未调用函数,则链接器可以从最终可执行文件中删除函数体。
由于上述原因以及其他原因,我们真正需要看到一些实际的代码才能诊断您的问题。

谢谢,我已经添加了代码,请查看更新。你说得很对,编译器实际上无法优化它,因为它不知道外部会调用什么。所以我不知道为什么启用优化编译会出错,而不启用优化就没问题。 - Frank
实际上,编译器可以看到 Foo<int> v 从未被使用,并将其优化掉。 - Frank
当我没有进行优化编译时,nm工具在foo-instantiations.o中显示3个符号:_Z11neverCalledv、_ZN3FooIiEC1Ei、__gxx_personality_v0。但是当我使用-O2进行编译时,只有一个符号:_Z11neverCalledv。因此...Foo...不再存在。 - Frank
请查看我的有关ODR的帖子。 - anon
编译器可以知道局部变量永远不会被使用并将其优化掉,留下一个无法删除的空函数体,但是你失去了实例化。 - David Rodríguez - dribeas
链接器可以优化符号(实例化函数)的消除,但由于实际上被使用(链接器在抱怨模板构造函数未定义),它不会优化实例化的模板的消除。 - David Rodríguez - dribeas

2
编译器正在优化一个从未使用的变量,但它无法基于函数不会被使用的理由来进行优化,因为该函数可能会在不同的编译单元中使用。您可以尝试通过类似以下内容的方式强制编译器将变量视为已使用:
void instantiation()
{
   Foo<int> f;
   f; // mark the variable as if it is used.
}

// or:
Foo<int>* instantiation()
{
   Foo<int> *p = new Foo<int>();
   return p; // The compiler cannot know if p will be used, it must compile
}

更好的解决方案是,如果你想要的话,明确实例化模板:
// .h
template <typename T>
class Foo
{
public:
   Foo( T const & value );
   void set( T const & ); // whatever else
private:
   T value_;
};

// template implementation another file, not included from .h
// instantiation.cpp??
template <typename T>
Foo<T>::Foo<T>( T const & value ) : value_(value) {}

template <typename T>
void Foo<T>::set( T const & v )
{
   value_ = value;
}

// explicit instantiation
template class Foo<int>;
template class Foo<double>;

// test.cpp
#include "header.h"
int main()
{
    Foo<int> f(5);
    f.set( 7 );

    Foo<char> f2; // linker error Foo<char>() not defined
}

用户代码只能看到头文件并知道哪些方法存在,但不能看到真正的实现。实现将在一个编译单元中编译,其中显式模板实例化会发生。

请注意,如果您忘记显式实例化某个类型,则会出现链接错误,而不是编译错误。

单一定义规则

c++中的单一定义规则指每个符号或类只能有一个定义。对于常规符号,多个定义可以很容易地检测出来(如果您定义了两个void f() { },链接器将检测到重复的符号),但对于模板来说就有点棘手了。由于模板通常在头文件中声明和定义,因此使用的符号在每个编译单元[1]中都会生成,链接器通常会找到多个等效符号(std::vector::push_back()被编译为每个具有std::vector并调用push_back的编译单元)。

编译器将模板代码标记为“弱”符号,表示虽然该符号在此处定义,但也可以在另一个编译单元中定义,并且链接器可以自由地丢弃该符号而不产生链接错误。如果要链接使用相同STL工具的不同编译单元(例如,使用相同类型),则必须满足此要求。

在gcc 4.2之前,gcc linux链接器会丢弃所有但一个弱符号,而不进行进一步检查。某些链接器(如linux中的gcc链接器)将在不久的将来(不是4.2,不知道4.3或4.4是否已经有了)检查不同的“弱”符号是否实际上相同,并向用户提供错误/警告。

您的代码正在破坏ODR,因为您正在不同的位置重新声明模板。您应该只声明一次模板,然后按照上面发布的方法外部实现方法。无论如何,如果两个定义是兼容的(正如您发布的片段中一样):所有成员方法和属性完全相同,并且具有相同的限定符(虚拟/常量...),则编译器应该接受它,因为只有一个模板的定义(重复)。

[1] 只有在代码中实际调用的那些方法才会被编译:

template <typename T>
struct Test
{
   void f() { std::cout << "f()" << std::endl; }
   void g() { std::cout << "g()" << std::endl; }
};
int main()
{
   Test<int> t;
   t.f(); // compiler generates Test<int>::f, but not Test<int>::g
}

2

#pragma的主题下搜索文档。这个定义是一种逃生舱口,它允许您指定各种属性。 GCC支持,因此g++也有很大的可能性支持。请注意,这些可能不是可移植的,这可能对您的项目重要性或无关紧要。


这些不是标准的。#pragma 是一个预编译器关键字,用于更改编译器的实现细节。如果您依赖 #pragma,则会失去可移植性。 - David Rodríguez - dribeas
我提到它们可能不具备可移植性,但对于某些项目来说这并不是一个问题。我知道这一点,并且已经明确说明了。我让问题的提出者自行决定是否适合他们的项目。 - MikeJ

1

这通常是通过编译器指令完成的。在C语言中,它将是一个#pragma,在Delphi Pascal中,它是{$O-},{$O+},围绕相关代码。具体的语法和方法是实现特定的,因此需要查看您正在使用的任何系统的文档。

不优化函数很简单,但有时需要告诉编译器不要优化特定的代码。这是非常罕见的事情,我已经很久没有遇到过了,但偶尔会发生。通常发生这种情况的情况是,您正在针对一些旧的遗留代码进行编译,这些代码是在后来的CPU技术发展之前构建的 - 超线程是一个典型的例子。


0

将您的变量声明为volatile:

volatile Foo<int> v;

通常情况下,它会防止任何优化。我已经使用英特尔C++编译器和Microsoft Visual Studio 2008进行了检查。

0

我一时半会儿不太确定。也许 预编译头文件 可以解决这个问题呢?

稍作修改,显然这不能解决在代码中使用较小的模板头文件的问题,但它可能有助于编译时间问题(从而消除对模板的需求)。


编译器在不知道模板将被定义为哪些类型时无法“预编译”模板。 - David Rodríguez - dribeas
@DavidRodríguez-dribeas - 实际上,它可以进行“预编译”。但是,“预编译”有点不准确,更像是“预解析和评估”,而不是真正的编译。编译器仍然可以完成将模板代码转换为其自己内部表示模板的所有工作。 - Grant Peters

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接