C#编译器和优化器会破坏这段代码吗?

23

在一个函数中给出以下C#代码:

....
var documentCollection =
    client.CreateDocumentCollectionQuery("dbs/" + database.Id)
        .Where(c => c.Id == DocumentCollectionName)
        .AsEnumerable()
        .FirstOrDefault();

if (documentCollection == null)
{
    documentCollection =
        await
        client.CreateDocumentCollectionAsync(
            "dbs/" + database.Id,
            new DocumentCollection { Id = DocumentCollectionName });
}

return client;

注意:我不会返回documentCollection,我只需要将其初始化(如果尚未初始化)(使用CreateDocumentCollectionAsync调用)。因此,在if块之后,documentCollection变成了一个未使用的变量。

现在,ReSharper建议进行优化:

var documentCollection =
    client.CreateDocumentCollectionQuery("dbs/" + database.Id)
        .Where(c => c.Id == DocumentCollectionName)
        .AsEnumerable()
        .FirstOrDefault()
    ?? await
        client.CreateDocumentCollectionAsync(
            "dbs/" + database.Id,
            new DocumentCollection { Id = DocumentCollectionName });

现在表明documentCollection是一个未使用的变量。

我的问题: C#代码优化或“发布”版本是否会完全删除这行代码,导致CreateDocumentCollectionAsync无法触发?

C#优化课程教给我,在“发布”版本中,只要函数中不需要变量“down the line”,就会立即垃圾回收它们,而调试版本不会这样做(为了进行调试)。

我现在想知道,它是否会如此热心地优化掉一个未使用的变量赋值(触发后台操作)。


6
我相信它能优化未使用的变量赋值,但不能优化触发的异步操作。如果CreateDocumentCollectionAsync导致任何副作用,这些副作用应始终对您的程序可见。 - Douglas
1
未使用的变量分配如何“触发后台操作”? - Jon Hanna
1
最后一句话问道:“我现在想知道它是否如此渴望,甚至会优化掉一个未使用的变量赋值”,它可能确实会这样做,但这并不重要。 - Jon Hanna
2
你所说的一个release-mode程序会尽可能地积极收集仅剩下在从未再使用的变量中的对象的陈述是不正确的; 垃圾回收器不必须这样做。相反,垃圾回收器被允许这样做。有时它会这样做,有时不会,这取决于诸如变量是否已注册等因素。 - Eric Lippert
5
这里的相关问题不是因为其返回值未被使用而呼叫是否会被优化掉。如果呼叫已经是“死”的,而不是其结果未被使用,虚拟机才可能这样做。更有趣的问题是涉及的对象是否具有终结器。如果是这样,那么垃圾收集器可以在变量被优化掉之前完成对其的终结操作。实际上,如果GC确定该对象永远不会被引用,终结器甚至可以与其构造函数并发运行。这是一种非常奇怪的情况。 - Eric Lippert
显示剩余7条评论
3个回答

27

不,编译器和JIT都不会优化您的方法调用。

这里有一个JIT编译器能够做到的列表,例如它可以优化掉 if (false) { ... } 语句块或未使用的变量赋值等。但它不会仅仅优化掉您的方法调用。如果是这样的话,每次对 void 方法的调用也应该不存在了。


4
假设我找到的清单是正确的,那么该清单既不是确定的也不是完整的,肯定会随着优化而改变,因为优化只是实现细节。编译器必须遵守的规则(忽略并发)可以简化为“任何优化只要不改变可观察的程序行为就是有效的”。因此,如果编译器能够证明调用没有可观察的效果,那么它就可以将它们优化掉。 - Voo

16

编号。

任何优化器只能删除没有可观察行为的代码。

否则它不能称为优化器。


1
正确。这是真正的答案。其他答案大多指向实现细节。这个答案更好,因为它通过契约进行论证。 - usr
1
@usr:如果你想用不同的措辞表达相同的意思,请随意编写自己的内容 :P - Lightness Races in Orbit
似乎是浪费。 - usr
好的,+1。只有一个问题(与问题无关)。你是怎么学习英语的?参考资料是什么? - Shafizadeh
我想知道,你怎么知道我会波斯语?(看来英语是你的母语。祝贺你) - Shafizadeh
@Shafizadeh:如果你不会说波斯语,我想你在伊朗社会中不会是一个非常有效的成员。 - Lightness Races in Orbit

15

免责声明: 这是一个可能会发生变化的实现细节,应该谨慎对待。


CLI规范中的ECMA-335,第I.12.6.4节(优化)陈述如下:

符合CLI的实现可以使用任何保证在单个执行线程内,在CIL指定的顺序中可见由线程生成的副作用和异常的技术来执行程序。仅限于此目的,易失性操作(包括易失性读取)构成可见副作用。(请注意,虽然只有易失性操作构成可见副作用,但易失性操作也影响非易失性引用的可见性。)易失性操作在§I.12.6.7中指定。与由另一个线程注入到线程中的异常相对于排序没有保证(此类异常有时称为“异步异常”(例如System.Threading.ThreadAbortException)。

[理由:优化编译器可以自由地重新排列副作用和同步异常,只要此重新排列不改变任何可观察的程序行为即可。结束理由]

[注意:CLI的实现允许使用优化编译器,例如将CIL转换为本机机器代码,前提是编译器保持(在每个单独的执行线程中)相同的副作用和同步异常顺序。这是一个比ISO C++(允许在序列点之间进行重新排序)或ISO Scheme(允许对函数的参数进行重新排序)更强的条件。结束注意]

这意味着符合CLI的任何实现都可以进行此类优化,如果它能保证副作用的顺序不会受到影响。这意味着如果一个方法没有副作用,并且JIT或语言编译器在静态分析中表明对于给定的事实,它可能被优化掉,因为该方法无论是否使用都不会有副作用的重排。

话虽如此,目前,C#编译器优化掉未使用的变量,但不会优化掉方法调用。编译器没有对整个方法调用做静态分析,所以无法证明该方法在代码中没有副作用。此外,JIT优化并不那么积极,它可能只是内联方法调用,但不能将其优化掉。

作为开源项目,您可以查看x86 JIT编译阶段并通过compphases.h获取一些正在执行的优化:

// Names of x86 JIT phases, in order.  Assumes that the caller defines CompPhaseNameMacro
// in a useful way before including this file, e.g., to define the phase enumeration and the
// corresponding array of string names of those phases.  This include file undefines CompPhaseNameMacro
// after the last use.
// The arguments are:
//   CompPhaseNameMacro(enumName, stringName, hasChildren, parent)
//     "enumName" is an Enumeration-style all-caps name.
//     "stringName" is a self-explanatory.
//     "hasChildren" is true if this phase is broken out into subphases.
//         (We should never do EndPhase on a phase that has children, only on 'leaf phases.')
//     "parent" is -1 for leaf phases, otherwise it is the "enumName" of the parent phase.

CompPhaseNameMacro(PHASE_PRE_IMPORT,             "Pre-import",                     "PRE-IMP",  false, -1)
CompPhaseNameMacro(PHASE_IMPORTATION,            "Importation",                    "IMPORT",   false, -1)
CompPhaseNameMacro(PHASE_POST_IMPORT,            "Post-import",                    "POST-IMP", false, -1)
CompPhaseNameMacro(PHASE_MORPH,                  "Morph",                          "MORPH",    false, -1)
CompPhaseNameMacro(PHASE_GS_COOKIE,              "GS Cookie",                      "GS-COOK",  false, -1)
CompPhaseNameMacro(PHASE_COMPUTE_PREDS,          "Compute preds",                  "PREDS",    false, -1)
CompPhaseNameMacro(PHASE_MARK_GC_POLL_BLOCKS,    "Mark GC poll blocks",            "GC-POLL",  false, -1)
CompPhaseNameMacro(PHASE_COMPUTE_EDGE_WEIGHTS,   "Compute edge weights (1)",       "EDG-WGT",  false, -1)
#if FEATURE_EH_FUNCLETS
CompPhaseNameMacro(PHASE_CREATE_FUNCLETS,        "Create EH funclets",             "EH-FUNC",  false, -1)
#endif // FEATURE_EH_FUNCLETS
CompPhaseNameMacro(PHASE_OPTIMIZE_LAYOUT,        "Optimize layout",                "LAYOUT",   false, -1)
CompPhaseNameMacro(PHASE_OPTIMIZE_LOOPS,         "Optimize loops",                 "LOOP-OPT", false, -1)
CompPhaseNameMacro(PHASE_CLONE_LOOPS,            "Clone loops",                    "LP-CLONE", false, -1)
CompPhaseNameMacro(PHASE_UNROLL_LOOPS,           "Unroll loops",                   "UNROLL",   false, -1)
CompPhaseNameMacro(PHASE_HOIST_LOOP_CODE,        "Hoist loop code",                "LP-HOIST", false, -1)
CompPhaseNameMacro(PHASE_MARK_LOCAL_VARS,        "Mark local vars",                "MARK-LCL", false, -1)
CompPhaseNameMacro(PHASE_OPTIMIZE_BOOLS,         "Optimize bools",                 "OPT-BOOL", false, -1)
CompPhaseNameMacro(PHASE_FIND_OPER_ORDER,        "Find oper order",                "OPER-ORD", false, -1)
CompPhaseNameMacro(PHASE_SET_BLOCK_ORDER,        "Set block order",                "BLK-ORD",  false, -1)
CompPhaseNameMacro(PHASE_BUILD_SSA,              "Build SSA representation",       "SSA",      true,  -1)
CompPhaseNameMacro(PHASE_BUILD_SSA_TOPOSORT,     "SSA: topological sort",          "SSA-SORT", false, PHASE_BUILD_SSA)
CompPhaseNameMacro(PHASE_BUILD_SSA_DOMS,         "SSA: Doms1",                     "SSA-DOMS", false, PHASE_BUILD_SSA)
CompPhaseNameMacro(PHASE_BUILD_SSA_LIVENESS,     "SSA: liveness",                  "SSA-LIVE", false, PHASE_BUILD_SSA)
CompPhaseNameMacro(PHASE_BUILD_SSA_IDF,          "SSA: IDF",                       "SSA-IDF",  false, PHASE_BUILD_SSA)
CompPhaseNameMacro(PHASE_BUILD_SSA_INSERT_PHIS,  "SSA: insert phis",               "SSA-PHI",  false, PHASE_BUILD_SSA)
CompPhaseNameMacro(PHASE_BUILD_SSA_RENAME,       "SSA: rename",                    "SSA-REN",  false, PHASE_BUILD_SSA)

CompPhaseNameMacro(PHASE_EARLY_PROP,             "Early Value Propagation",        "ERL-PROP", false, -1)
CompPhaseNameMacro(PHASE_VALUE_NUMBER,           "Do value numbering",             "VAL-NUM",  false, -1)

CompPhaseNameMacro(PHASE_OPTIMIZE_INDEX_CHECKS,  "Optimize index checks",          "OPT-CHK",  false, -1)

#if FEATURE_VALNUM_CSE
CompPhaseNameMacro(PHASE_OPTIMIZE_VALNUM_CSES,   "Optimize Valnum CSEs",           "OPT-CSE",  false, -1)
#endif  

CompPhaseNameMacro(PHASE_VN_COPY_PROP,           "VN based copy prop",             "CP-PROP",  false, -1)
#if ASSERTION_PROP
CompPhaseNameMacro(PHASE_ASSERTION_PROP_MAIN,    "Assertion prop",                 "AST-PROP", false, -1)
#endif
CompPhaseNameMacro(PHASE_UPDATE_FLOW_GRAPH,      "Update flow graph",              "UPD-FG",   false, -1)
CompPhaseNameMacro(PHASE_COMPUTE_EDGE_WEIGHTS2,  "Compute edge weights (2)",       "EDG-WGT2", false, -1)
CompPhaseNameMacro(PHASE_DETERMINE_FIRST_COLD_BLOCK, "Determine first cold block", "COLD-BLK", false, -1)
CompPhaseNameMacro(PHASE_RATIONALIZE,            "Rationalize IR",                 "RAT",      false, -1)
CompPhaseNameMacro(PHASE_SIMPLE_LOWERING,        "Do 'simple' lowering",           "SMP-LWR",  false, -1)

CompPhaseNameMacro(PHASE_LCLVARLIVENESS,         "Local var liveness",             "LIVENESS", true, -1)
CompPhaseNameMacro(PHASE_LCLVARLIVENESS_INIT,    "Local var liveness init",        "LIV-INIT", false, PHASE_LCLVARLIVENESS)
CompPhaseNameMacro(PHASE_LCLVARLIVENESS_PERBLOCK,"Per block local var liveness",   "LIV-BLK",  false, PHASE_LCLVARLIVENESS)
CompPhaseNameMacro(PHASE_LCLVARLIVENESS_INTERBLOCK,  "Global local var liveness",  "LIV-GLBL", false, PHASE_LCLVARLIVENESS)

CompPhaseNameMacro(PHASE_LVA_ADJUST_REF_COUNTS,  "LVA adjust ref counts",          "REF-CNT",  false, -1)

#ifdef LEGACY_BACKEND
CompPhaseNameMacro(PHASE_RA_ASSIGN_VARS,         "RA assign vars",                 "REGALLOC", false, -1)
#endif // LEGACY_BACKEND
CompPhaseNameMacro(PHASE_LOWERING_DECOMP,        "Lowering decomposition",         "LWR-DEC",  false, -1)
CompPhaseNameMacro(PHASE_LOWERING,               "Lowering nodeinfo",              "LWR-INFO", false, -1)
#ifndef LEGACY_BACKEND
CompPhaseNameMacro(PHASE_LINEAR_SCAN,            "Linear scan register alloc",     "LSRA",     true, -1)
CompPhaseNameMacro(PHASE_LINEAR_SCAN_BUILD,      "LSRA build intervals",           "LSRA-BLD", false, PHASE_LINEAR_SCAN)
CompPhaseNameMacro(PHASE_LINEAR_SCAN_ALLOC,      "LSRA allocate",                  "LSRA-ALL", false, PHASE_LINEAR_SCAN)
CompPhaseNameMacro(PHASE_LINEAR_SCAN_RESOLVE,    "LSRA resolve",                   "LSRA-RES", false, PHASE_LINEAR_SCAN)
#endif // !LEGACY_BACKEND
CompPhaseNameMacro(PHASE_GENERATE_CODE,          "Generate code",                  "CODEGEN",  false, -1)
CompPhaseNameMacro(PHASE_EMIT_CODE,              "Emit code",                      "EMIT",     false, -1)
CompPhaseNameMacro(PHASE_EMIT_GCEH,              "Emit GC+EH tables",              "EMT-GCEH", false, -1)
一些优化包括: - 消除死代码 - 线性扫描寄存器分配 - 循环展开 - 范围检查消除 这篇文章继续描述了JIT执行的一些优化,@EricLippert在这里对优化进行了普遍讨论。

1
它可能会优化掉对[Pure]方法的调用吗? - IS4
2
@IllidanS4 如果该方法被标记为[Pure]并且返回值被丢弃,则可能会被视为死代码消除。更多信息请参见此处 - Yuval Itzchakov

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接