【问题标题】:Is there any run-time overhead to readonly?只读是否有任何运行时开销?
【发布时间】:2009-05-27 00:17:19
【问题描述】:

出于某种原因,我一直认为readonly 字段具有与之相关的开销,我认为这是CLR 跟踪readonly 字段是否已初始化。这里的开销将是一些额外的内存使用来跟踪状态并在分配值时进行检查。

也许我假设这是因为我不知道 readonly 字段只能在构造函数内或字段声明本身内初始化,并且如果没有运行时检查,您将无法保证它不会被以各种方法多次分配。但是现在我知道了,C# 编译器可以很容易地对其进行静态检查,对吧?是这样吗?

另一个原因是我读到readonly 的使用对性能有“轻微”影响,但他们从未提出过这种说法,我找不到关于这个主题的信息,因此我提出了问题。我不知道除了运行时检查之外还有什么其他的性能影响。

第三个原因是我看到 readonly 在编译后的 IL 中保留为 initonly,那么如果 readonly 只不过是由C# 编译器,该字段从未分配给构造函数或声明之外?

另一方面,我发现您可以通过反射设置readonly int 的值,而CLR 不会抛出异常,如果readonly 是运行时检查,这应该是不可能的。

所以我的猜测是:“只读”只是一个编译时特性,任何人都可以确认/否认这一点吗?如果是,那么将这些信息包含在 IL 中的原因是什么?

【问题讨论】:

    标签: c# performance readonly immutability


    【解决方案1】:

    您必须从与访问修饰符相同的角度来看待它。访问修饰符存在于 IL 中,但它们真的是运行时检查吗? (1) 我不能在编译时直接分配私有字段,(2) 我可以使用反射来分配它们。到目前为止,似乎没有运行时检查,例如 readonly

    但是让我们来看看访问修饰符。执行以下操作:

    1. 使用公共类 C 创建程序集 A.dll
    2. 创建引用 A.dll 的程序集 B.exe。 B.exe 使用 C 类。
    3. 构建两个程序集。运行 B.exe 就可以了。
    4. 重建 A.dll,但将 C 类设置为内部。替换 B.exe 目录下的 A.dll。

    现在,运行 B.exe 会引发运行时异常。

    访问修饰符也存在于 IL 中,对吗?那么他们的目的是什么?目的是引用 .Net 程序集的其他程序集需要知道它们被允许访问的内容和不允许访问的内容,包括编译时和运行时。

    Readonly 在 IL 中似乎有类似的目的。它告诉其他程序集是否可以写入特定类型的字段。但是,readonly 似乎没有 具有与上面示例中的访问修饰符相同的运行时检查。似乎 readonly 是编译时检查,不会发生在运行时。在此处查看性能示例:Read-only performance vs const

    同样,这并不意味着 IL 没有用。 IL 确保首先发生编译时错误。请记住,在构建时,您不是针对代码进行构建,而是针对程序集进行构建。

    【讨论】:

    • 哎呀,你发现了和我一样的性能分析。所以 +1 给你,我已经删除了我的(不太详细的)答案。
    【解决方案2】:

    如果您使用标准的实例变量,readonly 将与普通变量几乎相同。添加的 IL 成为编译时检查,但在运行时几乎被忽略。

    如果您使用的是静态只读成员,情况会有所不同...

    由于在静态构造函数期间设置了静态只读成员,JIT“知道”一个值存在。没有额外的内存 - readonly 只是阻止其他方法设置它,但这是编译时检查。

    因为 JIT 知道这个成员永远不会改变,它在运行时被“硬编码”,所以最终效果就像拥有一个 const 值。不同之处在于 JIT 时间本身需要更长的时间,因为 JIT 编译器需要做额外的工作来将 readonly 的值硬连接到位。 (不过,这将非常快。)

    Expert C++/CLI by Marcus Hegee 对此有相当好的解释。

    【讨论】:

      【解决方案3】:

      任何其他答案尚未提及的重要一点是,当访问只读字段或访问任何属性时,使用数据副本满足请求。如果所讨论的数据是具有超过 4-8 字节数据的值类型,则这种额外复制的成本有时可能会很大。请注意,虽然当结构从 16 字节增长到 17 字节时成本会大幅增加,但在许多应用程序中,结构可能会更大一些并且仍然比类更快,如果它们不经常复制。例如,如果一个人应该有一种表示三维空间中三角形顶点的类型。一个简单的实现是一个结构体,它包含一个结构体,每个点都有三个float;总共可能 36 个字节。如果点和每个点内的坐标是可变的公共字段,则可以快速轻松地访问someTriangle.P1.X,而无需复制除顶点 1 的 Y 坐标之外的任何数据。另一方面,如果 P1 是一个属性或readonly 字段,编译器必须将P1 复制到一个临时结构,然后从中读取X

      【讨论】:

      • 这可能是一个非常古老的问题,但如果任何未来的读者感兴趣,Jon Skeet 的以下博客条目包含与此答案相关的更多详细信息:codeblog.jonskeet.uk/2014/07/16/…
      • @KlitosKyriacou:我想我以前没有看过那个帖子。我真的希望 .NET 提供了一种方法,通过该方法,结构方法可以承诺不写 this 或明确表示它们这样做,因为这样可以(1)在许多情况下调用不写的方法时提高性能写this [如博客中所述],以及(2) 允许安全地拥有写this 的方法,并让编译器拒绝在非变量上调用它们的尝试。在许多情况下,语义上最合乎逻辑的数据类型是可变结构,因为...
      • ...模式structVar = structVar.withSomeChange(); 不能成为线程安全的,但structVar.makeSomeChange() 可以[使用Interlocked.CompareExchange]。不幸的是,如果一个类公开了一个 [假设的] 结构类型 AtomicBitVector32 的属性,如果 Flags 是一个属性而不是一个字段,编译器就会默默地为像 thing.flags.TestAndSetBit(23); 这样的操作生成虚假代码。
      【解决方案4】:

      即使 readonly 仅在编译时生效,仍然需要将数据存储在程序集(即 IL)中。 CLR 是一种通用 语言运行时 - 用一种语言编写的类可以被其他语言使用和扩展。

      由于 CLR 的每个编译器都不知道如何读取和编译所有其他语言,为了保留 readonly 字段的语义,这些数据需要存储在程序集中,以便其他语言的编译器语言会尊重它。

      当然,字段被标记为readonly 的事实意味着 JIT 可以做其他事情,例如优化(例如内联使用值)等。不管您使用反射来更改字段值的事实如何,创建 IL 来修改相应构造函数之外的 initonly 字段(实例或静态,取决于字段类型),将导致无法验证的程序集。

      【讨论】:

        猜你喜欢
        • 2012-03-12
        • 2013-07-06
        • 1970-01-01
        • 1970-01-01
        • 2022-08-18
        • 2016-01-24
        • 2011-01-03
        • 1970-01-01
        相关资源
        最近更新 更多