【问题标题】:Is it possible to have a compiler which optimizes a = func(a)? [closed]是否有可能有一个优化 a = func(a) 的编译器? [关闭]
【发布时间】:2013-07-03 14:31:24
【问题描述】:

假设我有一个 A 类型的对象。考虑这种情况,适用于 A -> A 类型的任何函数(即获取 A 类型的对象并返回另一个 A 类型的对象):

foo = func(foo)

在这里,最简单的情况是将func(foo) 的结果复制到foo。 是否可以对此进行优化:

  • foofunc 中就地修改

对使用的语言没有限制。我想知道的是语言必须具有哪些约束和属性才能实现这种优化。是否有任何现有的语言可以执行这种优化?

示例(伪代码):

type Matrix = List<List<int>>

Matrix rotate90Deg(Matrix x):
   Matrix result(x.columns, x.rows) #Assume it has a constructor which takes as args the num of rows, and num of cols.
   for (int i = 0; i < x.rows; i++):
       for (int j = 0; j < x.columns; j++):
           result[i][j] = x[j][i]
   return result

Matrix a = [[1,2,3],[4,5,6],[7,8,9]]
a = rotate90Deg(a)

在这里,是否可以优化代码,使其不为新矩阵(结果)分配内存,而只是修改传递的原始矩阵。

【问题讨论】:

  • 当然,当你内联函数调用时,你可以做到这一点,甚至更多。
  • 您能否详细说明内联函数如何帮助使函数就地生成?谢谢。
  • 考虑func(x) = x * 2 和代码foo = func(foo)。内联后是foo = foo * 2,在普通处理器上可以用ADD r1, r1这样的单指令实现。在更复杂的情况下也有可能,特别是如果foo 的地址不是"escape"
  • 在类 C 语言中,func 可以获取指向 foo 的指针,对 foo 进行操作并直接写回 foo。 void func(int* foo) { (*foo) += 10; }。 C# 对ref 参数执行相同的操作。这就是你的意思吗?
  • @antiduh 不,我不是这个意思。我编辑了问题并添加了一个示例。

标签: compiler-optimization


【解决方案1】:

首先,您必须意识到某些操作本质上不可能就地计算。矩阵-矩阵乘法就是一个例子,rotate90Deg 属于这一类,因为这样的操作实际上是矩阵乘以适当的乘法矩阵。

现在就您的示例而言,您实际上编写了一个 matrix transpose 函数。矩阵转置可以就地完成,因为您正在交换成对的数字,但我怀疑任何编译器都可以自动检测到这一点并为您优化它。确实,可以做很多很多的技巧来优化矩阵转置,以便对缓存友好,从而获得巨大的性能提升。然而,如果使用一个简单的实现,您几乎肯定会得到与 Aditya Kumar 在他的回答中描述的非常相似的东西。


正如我之前使用“朴素”这个词所预示的那样,程序员可以通过高级模板和其他元编程技术以极其优化的方式诱使编译器内联很多东西。 (至少在 C++ 中,可能还有其他允许您重载 operator = 的语言。)对于任何对如何完成以及所涉及内容的案例研究感兴趣的人,请查看 Eigen matrix library,以及它是如何处理的像u = v + w; 这样的简单操作,其中三个变量都是浮点矩阵。以下是关键点的简要概述。

一个简单的实现会重载operator+ 以返回一个临时值,而operator= 将该临时值复制到结果中。当然,在 C++11 中,通过移动构造函数在赋值过程中很容易避免最终复制,但是如果你有一些更复杂的东西,右手边有多个运算符,比如 @ 987654327@,因为每个运算符/方法都会返回一个临时值,并且必须循环该临时值才能处理下一个运算符。

长话短说,Eigen 所做的不是在发生相应的函数调用时执行每个操作,而是调用返回一个模板化的函子,该函子仅描述该操作这需要发生,所有实际工作最终都发生在 operator = 中,从而使编译器能够发出一个内联循环,只遍历一次数据并真正就地执行操作。

【讨论】:

    【解决方案2】:

    是的,这是可能的,并且这种优化至少由 C++11(内联)提供。

    稍微解释一下优化。

    例如

    foo_t foo;
    foo = func(foo); // #1
    foo_t func(foo_t foo1) {
       foo_t new_foo;
       // operate on new_foo by using foo1
       return new_foo;
    }
    

    foo_t 的三个实例正在生成:

    1. foo 被复制并作为 foo1 传递给 func
    2. new_foo 已创建。
    3. 通过将new_foo 的内容复制到foo 中,将new_foo 分配给foo

    只要有一些不变量,所有三个副本都可以消除。

    1. foo(要传递给函数的参数以后永远不会使用相同的原始值。这相当于说 foo 在第 1 行是“死的”。这是在这里建立的,因为 foo 被重新分配。
    2. 函数func 中对象new_foo 的范围有其生命周期,它不会延长函数func 的生命周期。这也是在这里建立的 new_foo 的创建方式,它将在堆栈上,并且堆栈中对象的生命周期与创建对象的函数的生命周期相同。

    在 C++ 中,可以使用内联函数 func 来实现。内联后的代码基本上是这样的。

    `foo_t foo;`
    `foo_t new_foo;`
    `// operate on new_foo by using foo`
    `foo = new_foo;`
    

    尽管 C++ 提供内联作为一种语言功能,但如今几乎所有优化编译器都进行内联。

    现在取决于您对new_foofoo 执行的操作类型,这额外的new_foo 是否会被优化掉。对于某些数据类型,这是微不足道的(编译器可以执行“复制传播”,然后执行“死代码消除”以完全删除 new_foo

    【讨论】:

    • 在上面的例子中,临时对象不能被消除,因为源和目标是相同的。修改目标会破坏源。
    • @HotLicks,你是对的。我已经删除了对移动语义的引用。
    猜你喜欢
    • 2011-03-30
    • 2015-06-16
    • 2013-06-17
    • 2014-05-23
    • 2011-08-02
    • 2013-11-26
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多