【问题标题】:Are there risks to optimizing code in C#?在 C# 中优化代码是否存在风险?
【发布时间】:2012-01-18 00:26:49
【问题描述】:

在VS2010 Pro的构建设置面板中,有一个CheckBox,标签为“优化代码”……我当然想检查一下……但是异常谨慎,我问了我的兄弟,他说它是未经检查的调试,并且在 C++ 中它可能会做一些会破坏或错误代码的事情......但他不了解 C#。

所以我的问题是,我可以为我的发布版本选中此框而不用担心它会破坏我的代码吗?其次,如果它可以破坏代码,何时以及为什么?欢迎提供解释链接。

【问题讨论】:

标签: c# .net optimization compiler-construction settings


【解决方案1】:

在我的情况下,当我打开优化标志时,它不会完成所有操作,因此最终结果中缺少测量点,所以我只是关闭了优化标志来修复错误:

using System.Threading.Tasks;

                Parallel.Invoke(
                    async () => await ProcessPartialArrayOperationAssets(operationAssets, 0, operationAssets.Count / 2,
                        operations, inspection1),
                    async () => await ProcessPartialArrayOperationAssets(operationAssets, operationAssets.Count / 2,
                        operationAssets.Count, operations, inspection1)
                );

private async Task ProcessPartialArrayInspectionOperations(IList<InspectionOperation> operations,
    int begin,
    int end,
    Inspection inspection,
    InspectionAsset inspectionAsset)
{
    await Task.Run(() =>
    {
        // create one new operation measuring point for each measuring point in the operation's equipment
        int itemCounter = begin + 1;

        for (int i = begin; i < end; i++)
        {
            lock (_thisLock)
            {
                InspectionOperation operation = operations[i];
                int itemNumber = 1;

                // get the asset
                InspectionAsset operationAsset = operation.OperationAsset;
                if (operationAsset != null)
                {
                    // get the measuring points
                    string ABAPTrue = Abap.ABAP_TRUE;

                    lock (_thisLock)
                    {
                        IList<MeasuringPoint> measuringPoints = DbContext.MeasuringPoints.Where(x =>
                                x.AssetID == operationAsset.AssetID && x.InactiveFlag != ABAPTrue)
                            .ToList();

                        if (measuringPoints != null)
                        {
                            //Debug.WriteLine("measuringPoints.Count = " + measuringPoints.Count);

                            // create the operation measuring points
                            foreach (MeasuringPoint measuringPoint in measuringPoints)
                            {
                                OperationMeasuringPoint operationMeasuringPoint =
                                    new OperationMeasuringPoint
                                    {
                                        InspectionID = inspection.InspectionID,
                                        OperationNumber = operation.OperationNumber,
                                        SubActivity = "",
                                        RoutingNo = "",
                                        ItemNumber = itemNumber.ToString("D4"),
                                        // e.g. "0001", "0002" and so on
                                        ItemCounter = itemCounter.ToString("D8"),
                                        // e.g. "00000001", "00000002" and so on
                                        MeasuringPointID = measuringPoint.MeasuringPointID,
                                        MeasuringPointDescription = measuringPoint.Description,
                                        Equipment = inspectionAsset.AssetID,
                                        Category = "P"
                                    };
                                DbContext.Entry(operationMeasuringPoint).State = EntityState.Added;
                                itemNumber++;
                                itemCounter++;
                            }
                        }
                    }
                }
            }
        }
    });
}

因此,我也用这个替换了 Parallel.Invoke 调用。仅供参考,使用 .NET Framework 4.7 时出现此问题。

await ProcessPartialArrayOperationAssets(operationAssets, 0, operationAssets.Count, operations, inspection1);

更新:

好的,我发现如果我从方法签名中删除async Task,我可以重新启用优化标志并使用Parallel.Invoke

    private void ProcessPartialArrayInspectionOperations(IList<InspectionOperation> operations,
        int begin,
        int end,
        Inspection inspection,
        InspectionAsset inspectionAsset)
    {
        // create one new operation measuring point for each measuring point in the operation's equipment
        int itemCounter = begin + 1;

        for (int i = begin; i < end; i++)
        {

            InspectionOperation operation = operations[i];
            int itemNumber = 1;

            // get the asset
            InspectionAsset operationAsset = operation.OperationAsset;
            if (operationAsset != null)
            {
                // get the measuring points
                string ABAPTrue = Abap.ABAP_TRUE;

                lock (_thisLock)
                {
                    IList<MeasuringPoint> measuringPoints = DbContext.MeasuringPoints.Where(x =>
                            x.AssetID == operationAsset.AssetID && x.InactiveFlag != ABAPTrue)
                        .ToList();

                    if (measuringPoints != null)
                    {
                        //Debug.WriteLine("measuringPoints.Count = " + measuringPoints.Count);

                        // create the operation measuring points
                        foreach (MeasuringPoint measuringPoint in measuringPoints)
                        {
                            OperationMeasuringPoint operationMeasuringPoint =
                                new OperationMeasuringPoint
                                {
                                    InspectionID = inspection.InspectionID,
                                    OperationNumber = operation.OperationNumber,
                                    SubActivity = "",
                                    RoutingNo = "",
                                    ItemNumber = itemNumber.ToString("D4"),
                                    // e.g. "0001", "0002" and so on
                                    ItemCounter = itemCounter.ToString("D8"),
                                    // e.g. "00000001", "00000002" and so on
                                    MeasuringPointID = measuringPoint.MeasuringPointID,
                                    MeasuringPointDescription = measuringPoint.Description,
                                    Equipment = inspectionAsset.AssetID,
                                    Category = "P"
                                };
                            DbContext.Entry(operationMeasuringPoint).State = EntityState.Added;
                            itemNumber++;
                            itemCounter++;
                        }
                    }
                }
            }
        }
    }

                        Parallel.Invoke(
                            () => ProcessPartialArrayInspectionOperations(operations, 0, operations.Count / 2,
                                inspection1, inspectionAsset),
                            () => ProcessPartialArrayInspectionOperations(operations, operations.Count / 2,
                                operations.Count, inspection1, inspectionAsset)
                        );

或者,我认为我可以为每个使用Task.Run,然后等待Task.WhenAll(t1, t2, t3);,如此处所述,但在这种情况下,我没有进行显式数据库调用,所以我认为它不适用于使用Task.Run而不是 Parallel.Invoke 虽然这个页面确实解释了为什么我的 Parallel.Invoke 没有完成:Parallel.Invoke does not wait for async methods to complete

详情请看《C#中的并发》https://stephencleary.com/book/

【讨论】:

    【解决方案2】:

    .net 编译器优化可能会导致错误。今天发生在我身上。我花了几个小时才搞定它。代码是:

    for (int i = 0; i < list.Count-1; i++) {
      list[i+1].DoSomeThing();
      //some code
      if (someCondition) {
        list.insert(i+1, new Item());
        i++;
      }
    }
    

    在某些时候,list[i+1] 被寻址为list[i],就好像两者都指向同一个项目。 这个错误太奇怪了。代码在调试模式和发布模式下运行良好,但是当我在 Visual Studio 之外运行它时,例如。从 .exe 文件中,代码崩溃了。只有关闭编译器优化才能修复它。

    【讨论】:

      【解决方案3】:

      例如,我有一段来自我硕士论文的一些模拟部分的代码。在打开优化标志的情况下,代码不会真正破坏程序,但路径查找器只执行一次运行和循环。 (递归代码将自身困在寻路者的一个循环中,它总是在优化标志关闭的情况下中断)。

      所以是的,优化标志有可能使软件表现不同。

      【讨论】:

        【解决方案4】:

        优化是否应该引入错误?没有。

        优化会引入错误吗?也许,毕竟没有什么是完美的。

        优化能否发现代码中一直存在但在关闭时隐藏的错误?当然,发生了很多。

        重要的是要意识到这是一种改变。就像您测试是否进行了很多更改一样,您应该在关闭它们时进行测试。如果 final-release 会打开它们,那么 final-test 也必须打开它们。

        【讨论】:

          【解决方案5】:

          您通常会在发布版本中使用此选项。这样做是安全和主流的。没有理由害怕发布启用了优化的代码。启用优化可能会干扰调试,这是在调试构建时禁用它的一个很好的理由。

          【讨论】:

          • IIRC 有一些边缘条件,当变量删除可能导致浮点计算给出不同的值(由于不强制它从本机大小减小)
          • @Marc 浮点代码与优化器的差异很常见,例如,(a+b)+c 不等于 a+(b+c) 和其他类似的 FP 怪癖.没什么好担心的。
          • 没那么简单。抖动是否会启用优化器主要取决于是否附加了调试器。注意工具 + 选项、调试、常规、“在模块加载时抑制 JIT 优化”设置。取消勾选它可以调试优化代码。
          • @hans 好的,但这与使用优化是否安全有点正交。
          • 与评估顺序无关。问题是 x86 FPU 有一堆精度为 80 位的寄存器。优化器使用堆栈来避免将计算结果存储回内存。效率更高,但中间结果不会被截断回 64 位。从而改变计算结果。 x64 抖动不是问题,它使用的是 64 位的 XMM 寄存器。这在当时听起来是个好主意:)
          【解决方案6】:

          在发布模式下运行时可能会出现一些在其他情况下不会出现的错误。臭名昭著的“非易失性标志”浮现在脑海:

          flag = false;
          
          Thread t = new Thread(
             o =>
             {
                  while(!flag)
                  {
                     // do stuff
                  }
             });
          t.Start();
          
          // main thread does some work
          
          flag = true;
          t.Join(); // will never return in release mode if flag is not volatile
          

          这是因为编译器优化造成的,因为 flag 变量被线程 t 的核心缓存,因此它看不到 flag 的更新值。

          【讨论】:

          • 该代码已损坏。它偶然在调试中起作用。在释放模式下,你的运气用完了。
          • @David Heffernan:嗯,我不认为它被破坏了。你为什么这么认为?这是众所周知的编译器/CPU 重新排序/缓存问题。
          • @tudor 您是否建议禁用优化以保证此代码的正确性并且是适当使用 volatile 的替代方法?
          • 代码被破坏,独立于任何编译器标志。即使在调试模式下,这也可能会导致问题(就像在优化代码中可能完全没问题一样)。启用优化可以使一些错误更加明显吗?当然可以,但它不会破坏有效代码。
          • 我认为它完全损坏了因为这是一个众所周知的编译器/CPU 重新排序/缓存问题。没有理由在不将flag 更改为易失性或插入Thread.MemoryBarrier() 的情况下返回该代码。幸运的调试版本意味着一个错误被隐藏了,而不是不存在。
          【解决方案7】:

          在 C# 中,优化永远不会破坏您的代码。

          相反,启用优化后,编译器在 C# 和 CIL 之间转换时会生成更紧凑的 CIL。

          我观察到(坦率地说,这很有趣!)来自 .NET

          【讨论】:

          • 还有人相信 C# 编译器的代码生成质量下降了吗?
          • 您的 CIL 讨论中有什么具体内容(最后一行)?
          • 您能举一个 CIL 质量较低的例子吗?你如何定义“好的 CIL”?
          • David:有几个新的翻译方案。例如,编译器现在将大部分中间计算存储在辅助局部变量中。然后,控制流包含更多的跳转,有时很难解释(比如 br 到下一条指令)。我可以提供示例,但制作一个简单的方法很容易,只需一个/两个“ifs”并比较两个编译器的输出。
          • @Wiktor:根据 IL 的外观来判断性能是很疯狂的。实际执行的是 jit 代码,而不是 IL。您是否考虑过您所描述的“臃肿”代码以及额外的本地代码等可能更容易处理抖动,并且实际上可能导致本地代码执行更好?
          【解决方案8】:

          优化不应真正破坏您的代码。 Eric Lippert 有一个post here,它解释了当你打开那个标志时会发生什么。性能提升会因应用程序而异,因此您需要使用您的项目对其进行测试,看看是否有任何明显的差异(在性能方面)。

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 2012-02-18
            • 1970-01-01
            • 1970-01-01
            • 2021-05-18
            • 2015-08-18
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多