14

我刚读到:

C++ 中的惰性求值

并注意到它有点旧,大多数答案都与 2011 年之前的 C++ 有关。现在我们有语法 lambdas,它甚至可以推断返回类型,所以惰性求值似乎可以归结为只是传递它们:而不是

auto x = foo();

你执行

auto unevaluted_x = []() { return foo(); };

然后评估您何时/何地需要:

auto x = unevaluted_x();

似乎没有更多的东西了。但是,其中一个答案建议使用带有异步启动的期货。有人可以用 C++ 或更抽象地说明为什么/如果未来对于惰性评估工作很重要吗?似乎未来很可能会被急切地评估,但简单地说,在另一个线程上,并且可能比创建它们的优先级低;无论如何,它应该依赖于实现,对吧?

此外,在惰性求值的上下文中,是否还有其他现代 C++ 结构有助于牢记?

4

3 回答 3

13

当你写

auto unevaluted_x = []() { return foo(); };
...
auto x = unevaluted_x();

每次您想要获取值时(当您调用 时unevaluated_x)都会计算它,从而浪费计算资源。因此,为了摆脱这种过多的工作,最好跟踪 lambda 是否已经被调用(可能在其他线程中,或者在代码库中非常不同的位置)。为此,我们需要一些 lambda 包装器:

template<typename Callable, typename Return>
class memoized_nullary {
public:
    memoized_nullary(Callable f) : function(f) {}
    Return operator() () {
        if (calculated) {
            return result;
        }
        calculated = true;
        return result = function();
    }
private:
    bool calculated = false;
    Return result;
    Callable function;
};

请注意,此代码只是一个示例,不是线程安全的。

但是,您不必重新发明轮子,而是可以使用std::shared_future

auto x = std::async(std::launch::deferred, []() { return foo(); }).share();

这需要更少的代码来编写并支持一些其他功能(例如,检查值是否已经计算,线程安全等)。

标准 [futures.async, (3.2)] 中有以下文本:

如果launch::deferred在策略中设置,则存储DECAY_COPY(std::forward<F>(f))DECAY_COPY(std::forward<Args>(args))...处于共享状态。这些副本f构成args了延迟函数。延迟函数的调用评估存储的值INVOKE(std::move(g), std::move(xyz))在哪里,并且存储的副本是任何返回值存储为共享状态中的结果。从延迟函数的执行传播的任何异常都作为异常结果存储在共享状态中。在函数完成之前,共享状态不会准备好。在引用此共享状态的异步返回对象上首次调用非定时等待函数 (30.6.4) 应调用调用等待函数的线程中的延迟函数gDECAY_COPY(std::forward<F>(f))xyzDECAY_COPY(std::forward<Args>(args))..... 一旦评估INVOKE(std::move(g),std::move(xyz))开始,该函数就不再被认为是延迟的。[注意:如果此策略与其他策略一起指定,例如在使用策略值时launch::async | launch::deferred,实现应推迟调用或在无法有效利用更多并发时选择策略。——尾注]

因此,您可以保证在需要之前不会调用计算。

于 2017-01-31T11:00:35.457 回答
5

这里发生了一些事情。

Applicative order评估意味着在将参数传递给函数之前对其进行评估。 Normal order评估意味着在评估它们之前将参数传递给函数。

正常顺序评估的好处是某些参数永远不会被评估,而缺点是某些参数会被一遍又一遍地评估。

Lazy评价通常是指normal order + memoization。推迟评估,希望您根本不必评估,但如果确实需要,请记住结果,这样您只需要做一次。重要的部分是永远或一次评估一个术语,记忆是提供这一点的最简单的机制。

promise/future模型又不同了。这里的想法是在您有足够的可用信息后立即开始评估,可能在另一个线程中。然后,您尽可能长时间地查看结果,以提高它已经可用的机会。


promise/future模型与惰性评估有一些有趣的协同作用。策略如下:

  1. 推迟评估直到肯定需要结果
  2. 在另一个线程中开始评估
  3. 做一些其他的事情
  4. 后台线程完成并将结果存储在某处
  5. 初始线程检索结果

当后台线程产生结果时,可以巧妙地引入记忆。

尽管两者之间有协同作用,但它们并不是同一个概念。

于 2017-01-31T11:30:54.557 回答
0

在多线程应用程序中,同时请求需要花费大量精力准备的数据,可以使用线程安全的记忆,这样用户不仅可以避免重做已经执行的工作,而且可以避免开始他们自己的已经完成的工作版本进步。

使用一个或多个未来来传递数据是很容易的部分:C++ 已经实现了。技巧是(a)找到一种方法来确保请求相同数据的多个线程创建将被视为映射中等效(或相同)键的对象......这取决于用户......和( b) 使用具有该键和未来作为数据的并发映射。最多一个同时执行的插入、emplace 或 try_emplace 尝试使用相同的键将能够插入一个键值对,并且它们都将返回一个迭代器到相同的键值对(这可能已经在映射中一段时间)。使用带有互斥锁的 std::unordered_map 会起作用,但它不能很好地扩展。Java 已经有在这些情况下具有出色性能的并发映射:C++ 也需要,

于 2020-10-23T16:49:12.477 回答