【问题标题】:C# optimizations and side effectsC# 优化和副作用
【发布时间】:2011-01-20 04:55:49
【问题描述】:

C# 编译器或 JITter 完成的优化是否有明显的副作用?

我已经举过一个例子。

var x = new Something();
A(x);
B(x);

当调用A(x) 时,x 保证会一直保持到A 的末尾——因为B 使用相同的参数。但是如果B被定义为

public void B(Something x) { }

然后B(x) 可以被优化器消除,然后可能需要调用GC.KeepAlive(x)

这种优化实际上可以由 JITter 完成吗?

除了堆栈跟踪更改之外,是否还有其他可能具有可见副作用的优化?

【问题讨论】:

  • 如果x不再被使用,为什么还要让它保持活跃?
  • 关于 JIT 优化的好文章请查看 codeproject.com/KB/dotnet/JITOptimizations.aspx - JITer 可能会优化掉空的方法调用,但会保留方法本身
  • @JSBangs:您可能希望让某些东西保持活动状态的原因是因为它的破坏会触发非托管代码在不希望的地方运行。例如,有些 COM 存储对象需要在包含它们的“外部”存储之前关闭“内部”存储(想想嵌套目录)。垃圾收集器可以确定不再使用代表外部存储的托管对象,并决定在清理仍然活动的内部存储之前清理它。在实践中,我不得不编写代码来处理这种情况,这真的很痛苦。
  • @Eric,但这不是IDisposable 的用途吗?外部存储持有对内部存储的引用,并在释放自己的资源之前对它们调用Dispose。希望您永远不必关心垃圾收集器何时声明某些内容。除非有问题的类是第 3 方库,否则您无法修改并且设计不佳,在这种情况下...... .
  • 我指的是与 Eric 提到的非托管代码有关的案例。我不知道具体细节,因为我只是从 Eric 那里听说过它们,但这就是我在示例中所考虑的。

标签: c# optimization jit


【解决方案1】:

如果你的函数 B 不使用参数 x,那么消除它并提前收集 x 不会有任何可见的副作用。

要成为“可见的副作用”,它们必须对程序可见,而不是对调试器或对象查看器等外部工具可见。

【讨论】:

    【解决方案2】:

    当调用 A(x) 时,x 保证保持活动到 A 的末尾 - 因为 B 使用相同的参数。

    这个说法是错误的。假设方法 A 总是抛出异常。抖动可以知道永远不会到达 B,因此可以立即释放 x。假设方法 A 在最后一次引用 x 后进入无条件无限循环;再次,抖动可以知道通过静态分析,确定 x 将永远不会被再次引用,并安排它被清理。我不知道抖动是否真的执行了这些优化;它们看起来很狡猾,但它们是合法的。


    这种优化(即,对未在任何地方使用的引用进行早期清理)实际上可以由 JITter 完成吗?

    是的,在实践中,它已经完成了。这不是可观察到的副作用。

    规范的第 3.9 节证明了这一点,为方便起见,我引用了该节:

    如果对象或其任何部分不能被任何可能的继续执行访问,除了析构函数的运行,该对象被视为不再使用,并且它有资格被销毁。 C# 编译器和垃圾收集器可能会选择分析代码以确定将来可能使用对对象的哪些引用。例如,如果作用域内的局部变量是对对象的唯一现有引用,但在从过程中的当前执行点开始的任何可能的继续执行中从未引用该局部变量,则垃圾收集器可能(但不需要)将对象视为不再使用。


    C# 编译器或 JITter 完成的优化是否有明显的副作用?

    您的问题已在规范的第 3.10 节中得到解答,为方便起见,我在此引用:

    继续执行 C# 程序 这样每个的副作用 执行线程保留在 关键执行点。

    一面 效果被定义为读或写 易失性字段,写入 非易失性变量,写入 外部资源,并抛出 一个例外。

    关键执行 这些点的顺序 必须保留的副作用是 对易失性字段、锁定语句的引用, 以及线程的创建和终止。

    执行环境是免费的 更改 C# 的执行顺序 程序,受制于以下 约束:

    数据依赖是 保存在一个线程中 执行。也就是说,每个值 变量被计算为好像所有 线程中的语句被执行 按原程序顺序。

    初始化排序规则是 保存。

    保留副作用的顺序 关于易失性读取和 写道。

    此外, 执行环境不需要 评估表达式的一部分,如果它 可以推断出该表达式的 值未使用且不需要 产生副作用(包括 任何由调用方法或 访问 volatile 字段)。

    什么时候 程序执行被中断 异步事件(例如 另一个线程抛出的异常), 不能保证 可观察到的副作用在 原节目顺序。

    倒数第二段是我相信你最关心的一段;也就是说,在影响可观察到的副作用方面,运行时允许执行哪些优化?允许运行时执行任何不影响可观察副作用的优化。

    请注意,特别是数据依赖性仅保留在一个执行线程中。当从另一个执行线程观察时,数据依赖性保证被保留。

    如果这不能回答您的问题,问一个更具体的问题。特别是,如果您认为上面给出的定义与您对“可观察到的副作用”的定义不匹配,则需要对“可观察到的副作用”进行仔细而准确的定义才能更详细地回答您的问题。

    【讨论】:

    • 由于我将参数传递给B,即使它没有在该方法中使用,我认为至少在输入 B 之前x 会被认为是有效的。我弄错了吗?
    • @configurator 我猜 JIT 可以看到该方法什么都不做,并完全消除该方法和对其的所有调用。
    • @configurator:正如第 3.9 节明确指出的那样,如果可证明从某个点过去从未读取过 x,则抖动没有义务考虑 x 的内容。抖动也没有义务将其视为已死。这是一个实现定义的优化。
    • 我现在明白我最初在哪里误读了。感谢您在这里提供的见解。
    【解决方案3】:

    在您的问题中包含B 只会混淆问题。鉴于此代码:

    var x = new Something();
    A(x);
    

    假设A(x) 是托管代码,那么调用A(x) 会维护对x 的引用,因此垃圾收集器在A 返回之前无法收集x。或者至少直到A 不再需要它。 JITer 所做的优化(没有缺陷)不会过早收集x

    您应该定义“可见的副作用”是什么意思。人们希望 JITer 优化至少具有使您的代码更小或更快的副作用。那些“可见”?还是您的意思是“不受欢迎”?

    【讨论】:

      【解决方案4】:

      Eric Lippert 开始了一个关于重构的精彩系列文章,这让我相信 C# 编译器和 JITter 确保不会引入副作用。 Part 1Part 2 目前在线。

      【讨论】:

        猜你喜欢
        • 2013-11-16
        • 2018-11-02
        • 2015-08-12
        • 2016-09-19
        • 2013-04-20
        • 1970-01-01
        • 1970-01-01
        • 2023-03-27
        相关资源
        最近更新 更多