【问题标题】:Remove null check after lazy initialization延迟初始化后删除空检查
【发布时间】:2015-09-29 06:46:22
【问题描述】:

当一个人决定使用延迟初始化时,他通常需要为此付费。

class Loafer
{
    private VeryExpensiveField field;
    private VeryExpensiveField LazyInitField()
    {
        field = new VeryExpensiveField();
        // I wanna here remove null check from accessor, but how?
        return field;
    }
    property Field { get { return field ?? LazyInitField(); } }
}

基本上,他必须每次检查他的支持字段是否具有 null/nil 值。如果他能摆脱这种做法呢?成功初始化字段后,就可以去掉这个检查了,对吧?

不幸的是,大多数生产语言不允许您在运行时修改它们的函数,尤其是在函数体中添加或删除单个指令,尽管如果明智地使用它会有所帮助。但是,在 C# 中,您可以使用 delegates(最初我发现它们,后来意识到为什么本地语言具有函数指针)和 events 机制来模仿这种行为,因此缺乏性能,因为空检查只是移动到较低级别,但不会完全消失。一些语言,例如LISP 和 Prolog,让您可以轻松修改它们的代码,但它们几乎不能被视为生产语言。

在 Delphi 和 C/C++ 等本地语言中,最好编写两个函数,安全和快速,通过指针调用它们并在初始化后将此指针切换到快速版本。您甚至可以允许编译器或 IDE 生成代码来执行此操作,而无需额外头痛。但是正如@hvd 提到的,这甚至会降低速度,因为CPU 不会知道这些功能几乎相同,因此不会将它们预取到它的缓存中。

是的,我是个性能狂,只为满足我的好奇心,在没有明确问题的情况下寻求性能。有哪些常用方法来开发此类功能?

【问题讨论】:

  • "并在初始化后切换到快速"... - 使用与 getter 中相同的条件语句。你想保存什么?几个 CPU 周期?
  • 调用函数代价高昂,至少与读取变量相比是这样。你知道 C 和 C++ 中的函数指针是什么吗?还是德尔福中的程序变量? IDE 如何与此处相关?你为什么提到它?性能问题通常需要比您提供的更多背景信息。
  • @Danatela 你衡量过它的影响吗?我问是因为我希望从内存中加载一个单词并将其与零进行比较比从内存中加载一个单词并将指令指针设置为该值稍微便宜一些,因为后者意味着处理器无法提前知道将执行哪些指令. (我也希望差异如此之小以至于很难衡量。)
  • @Danatela:在 C# 中,您可以以与本地语言中的函数指针相同的方式使用委托。但我怀疑是否会有任何性能提升。
  • 间接很少能提高性能。我怀疑函数调用是否比空检查更快。你为什么不测量?编写一些您希望优化的真实代码。

标签: c# c++ delphi lazy-initialization self-modifying


【解决方案1】:

实际上,当您将其开销与实际计算进行比较时,惰性工具包框架并不总是那么重要。

有很多方法。 您可以使用Lazy、一个自我修改的 lambda 设置、一个布尔值或任何最适合您的工作流程的东西。

只有在进行一些重复计算时才需要考虑惰性评估工具包的开销。

我的带有微型基准的代码示例探讨了在循环中伴随的更昂贵操作的上下文中惰性计算的相对开销。

你可以看到,惰性工具包的开销是可以忽略不计的,即使与相对芯片的有效负载操作一起使用。

void Main()
{
    // If the payload is small, laziness toolkit is not neglectible
    RunBenchmarks(i => i % 2 == 0, "Smaller payload");

    // Even this small string manupulation neglects overhead of laziness toolkit
    RunBenchmarks(i => i.ToString().Contains("5"), "Larger payload");
}

void RunBenchmarks(Func<int, bool> payload, string what)
{
    Console.WriteLine(what);
    var items = Enumerable.Range(0, 10000000).ToList();

    Func<Func<int, bool>> createPredicateWithBoolean = () =>
    {
        bool computed = false;
        return i => (computed || (computed = Compute())) && payload(i);
    };

    items.Count(createPredicateWithBoolean());
    var sw = Stopwatch.StartNew();
    Console.WriteLine(items.Count(createPredicateWithBoolean()));
    sw.Stop();
    Console.WriteLine("Elapsed using boolean: {0}", sw.ElapsedMilliseconds);

    Func<Func<int, bool>> createPredicate = () =>
    {
        Func<int, bool> current = i =>
        {
            var computed2 = Compute();
            current = j => computed2;
            return computed2;
        };
        return i => current(i) && payload(i);
    };

    items.Count(createPredicate());
    sw = Stopwatch.StartNew();
    Console.WriteLine(items.Count(createPredicate()));
    sw.Stop();
    Console.WriteLine("Elapsed using smart predicate: {0}", sw.ElapsedMilliseconds);
    Console.WriteLine();
}

bool Compute()
{
    return true; // not important for the exploration
}

输出:

Smaller payload
5000000
Elapsed using boolean: 161
5000000
Elapsed using smart predicate: 182

Larger payload
5217031
Elapsed using boolean: 1980
5217031
Elapsed using smart predicate: 1994

【讨论】:

  • 干得好!虽然我决定我不喜欢妥协:)
  • 现在,我会接受。 Wikipedia 文章解释说,现代 CPU 和内存架构无法实现真正​​的自我修改代码。
【解决方案2】:

FWIW 在 Spring4D 的帮助下也可以在 Delphi 中完成:

var
  field: Lazy<VeryExpensiveField>;
begin
  field :=
    function: VeryExpensiveField
    begin
      Result := VeryExpensiveField.Create;
    end;

【讨论】:

  • 这是否避免了问题中所述的空检查?
  • 它确实避免了 null 检查,就像接受的答案中提供的 Lazy&lt;T&gt; 类型的 C# 实现一样。
  • 我认为这也不能避免空检查。
猜你喜欢
  • 2020-05-10
  • 1970-01-01
  • 1970-01-01
  • 2011-11-17
  • 1970-01-01
  • 2019-09-26
  • 2020-10-08
  • 2017-11-11
  • 1970-01-01
相关资源
最近更新 更多