【问题标题】:Benefits of pure function纯函数的好处
【发布时间】:2012-06-24 14:39:27
【问题描述】:

今天我在阅读纯函数,对它的使用感到困惑:

如果函数为同一组输入返回同一组值并且没有任何可观察到的副作用,则称该函数为纯函数。

例如strlen() 是纯函数,rand() 是不纯函数。

__attribute__ ((pure)) int fun(int i)
{
    return i*i;
}

int main()
{
    int i=10;
    printf("%d",fun(i));//outputs 100
    return 0;
}

http://ideone.com/33XJU

上述程序的行为与没有pure 声明的情况相同。

将函数声明为pure[如果输出没有变化]有什么好处?

【问题讨论】:

  • 是的 - 查看生成的程序集。
  • 我不认为这个纯度的定义是正确的——例如printf 是合格的(用相同的参数调用它两次会产生相同的返回值),但它不是纯粹的。
  • @tdammers:确实,它缺少...and no side-effects... 部分。
  • @Ben:熵从何而来?我们在这里处理(理论上)确定性机器,获得真正熵的唯一方法是来自外部来源,这意味着副作用。当然,我们可以允许编程语言定义非确定性函数,假装技术副作用不存在,而函数确实是非确定性的;但如果我们这样做了,追踪纯度的大部分实际好处就会丧失。
  • tdammers 是正确的 - 上面给出的 pure 的定义是不正确的。纯意味着输出取决于函数的输入;此外,必须没有可观察到的副作用。 “相同输入的相同输出”是对这些要求的非常不准确的总结。 en.wikipedia.org/wiki/Pure_function

标签: c pure-virtual


【解决方案1】:

pure 让编译器知道它可以对函数进行某些优化:想象一下这样的代码

for (int i = 0; i < 1000; i++)
{
    printf("%d", fun(10));
}

使用纯函数,编译器可以知道它只需要评估fun(10) 一次,而不是1000 次。对于一个复杂的函数,这是一个巨大的胜利。

【讨论】:

  • 也就是说,你可以放心地使用 memoization
  • @mob 你什么意思?为什么不呢?
  • 因为你可以修改字符串(从某个地址开始的字符序列)而不修改输入(指向字符串开始地址的指针),也就是说,你不能记住它。它只会是具有不可变字符串的语言(例如 Java)中的纯函数。
  • @KonradRudolph:想象一个长度为 1000 的字符串。拨打strlen就可以了。然后再一次。一样的吗?现在将第二个字符修改为\0strlen 现在还返回 1000 吗?起始地址相同(== 输入相同)但函数现在返回不同的值。
  • @mob 这是一个很好的反对意见,显然你是对的。我被 even books 声称 strlen (在 GCC / glibc 中)实际上是纯的事实误导了。但是看一下 glibc 的实现表明这是错误的。
【解决方案2】:

当你说一个函数是“纯的”时,你保证它没有外部可见的副作用(正如评论所说,如果你撒谎,坏事可能会发生)。知道函数是“纯”的对编译器有好处,编译器可以利用这些知识进行某些优化。

以下是GCC documentation 关于pure 属性的说明:

纯的

许多函数除了返回值和它们的返回之外没有任何作用 value 仅取决于参数和/或全局变量。 这样的函数可以进行公共子表达式消除和 循环优化就像算术运算符一样。这些 函数应该用纯属性声明。例如,

          int square (int) __attribute__ ((pure));

Philip's answer 已经展示了知道一个函数是“纯”的如何有助于循环优化。

这是一个用于消除常见子表达式的方法(假设foo 是纯的):

a = foo (99) * x + y;
b = foo (99) * x + z;

可以变成:

_tmp = foo (99) * x;
a = _tmp + y;
b = _tmp + z;

【讨论】:

  • 我不确定是否有人这样做,但纯函数也允许编译器在函数被调用时重新排序,如果它认为重新排序是有益的。当存在副作用的可能性时,编译器需要更加保守。
  • @MPD - 是的,这听起来很合理。由于call 指令是超标量 CPU 的瓶颈,因此编译器的一些帮助会有所帮助。
  • 我隐约记得几年前使用 DSP 编译器会使用这种技术来早晚获得返回值。这使它能够最大限度地减少管道停顿。
  • 是否可以预先计算“foo(99)”,因为 99 是 const 并且 foo 将始终返回相同的结果?也许在某种两阶段编译中?
  • @markwatson - 我不确定。可能存在根本不可能的情况。例如如果foo 是另一个编译单元(另一个 C 文件)或预编译库的一部分。在这两种情况下,编译器都不知道foo 做了什么,也无法预先计算。
【解决方案3】:

除了可能的运行时优势之外,纯函数在阅读代码时更容易推理。此外,测试纯函数要容易得多,因为您知道返回值仅取决于参数的值。

【讨论】:

  • +1,您关于测试的观点很有趣。无需设置和拆卸。
【解决方案4】:

非纯函数

int foo(int x, int y) // possible side-effects

就像一个纯函数的扩展

int bar(int x, int y) // guaranteed no side-effects

除了显式函数参数 x、y、 宇宙的其余部分(或您的计算机可以与之通信的任何东西)作为隐含的潜在输入。同样,除了显式的整数返回值之外,您的计算机可以写入的任何内容都隐含地包含在返回值中。

应该清楚为什么推理纯函数比推理非纯函数要容易得多。

【讨论】:

  • +1:使用宇宙作为潜在输入是解释纯与非纯之间区别的一种非常好的方式。
  • 确实,这就是 monad 背后的理念。
【解决方案5】:

作为一个附加组件,我想提一下,C++11 使用 constexpr 关键字在某种程度上对事物进行了编码。示例:

#include <iostream>
#include <cstring>

constexpr unsigned static_strlen(const char * str, unsigned offset = 0) {
        return (*str == '\0') ? offset : static_strlen(str + 1, offset + 1);
}

constexpr const char * str = "asdfjkl;";

constexpr unsigned len = static_strlen(str); //MUST be evaluated at compile time
//so, for example, this: int arr[len]; is legal, as len is a constant.

int main() {
    std::cout << len << std::endl << std::strlen(str) << std::endl;
    return 0;
}

对 constexpr 使用的限制使得函数可以证明是纯的。这样,编译器可以更积极地优化(请确保您使用尾递归!)并在编译时而不是运行时评估函数。

所以,要回答您的问题,如果您使用 C++(我知道您说的是 C,但它们是相关的),以正确的样式编写纯函数允许编译器使用功能:-)

【讨论】:

    【解决方案6】:

    一般来说,纯函数比编译器可以利用的非纯函数有 3 个优势:

    缓存

    假设您有被调用 100000 次的纯函数 f,因为它是确定性的并且仅取决于其参数,编译器可以计算其值一次并在必要时使用它

    并行度

    纯函数不会读取或写入任何共享内存,因此可以在单独的线程中运行而不会产生任何意外后果

    通过引用传递

    函数f(struct t) 通过值获取其参数t,另一方面,如果将t 声明为纯函数,编译器可以通过引用f 来传递f,同时保证@987654326 的值@ 不会改变并且有性能提升


    除了编译时间方面的考虑之外,纯函数的测试也相当容易:只需调用它们即可。

    无需构造对象或模拟与数据库/文件系统的连接。

    【讨论】:

      猜你喜欢
      • 2013-04-28
      • 2014-04-19
      • 1970-01-01
      • 2011-06-08
      • 2020-03-19
      • 2023-03-18
      • 2016-08-07
      • 2014-04-29
      • 2010-10-14
      相关资源
      最近更新 更多