有没有一种方法可以在不使用GoTo语句的情况下编写这个东西?

6

编辑:这不是关于是否可以使用GoTo语句的问题。

这是一个关于如何处理.NET/IL中O(n^3)算法的核心部分而不使用GoTo语句的问题。请注意,追随Dijkstra哲学的信徒和同行,在未阅读问题之前请注意。

考虑以下代码,对于大多数用例,For o = 0 to nz循环的内容将被执行300万至1800万次之间。该子程序作为Parallel.For()调用的参数在我的代码中占据一席之地。 mnynz的范围都在10到300之间。

它经过手动优化以避免堆栈推送和子程序调用,换句话说,是为了速度。我的愿望是避免编译成包含内部循环中的callicall操作码的IL。

为了在满足测试条件时中止最内层的三个循环,我使用了一个GoTo语句来中止不需要的测试。

问题是,有没有一种方法可以不使用GoTo来编写这个代码?有没有一种方式可以编写这个代码,使得.NET JIT-Compiler将编译为更快的代码,而不会在目标代码中出现callcalli操作码?

Sub SomeLambda(m As Integer, newarray As Short(,,))
    For n = 0 To ny
        For o = 0 To nz
            If newarray(m, n, o) <> 1 AndAlso newarray(m, n, o) <> -1 Then
                For m1 = m - 1 To m + 1
                    For n1 = n - 1 To n + 1
                        For o1 = o - 1 To o + 1
                            If SomeCondition = True Then 'the array is not out of bounds '
                                Dim testVal = newarray(m1, n1, o1)
                                If testVal = -1 Then
                                    newarray(m, n, o) = -2
                                    GoTo Exitloopslabel2
                                End If
                            End If
                        Next
                    Next
                Next
   Exitloopslabel2: 
            End If
        Next
    Next
End Sub

大多数人会说,应该重构嵌套循环以避免使用goto,但如果你有理由相信子程序调用会太昂贵(即,你已经对其进行了测量),那么这确实是唯一的解决方案。 - siride
这不是任何其他“GoTo是否可行”的问题的副本。当然,在这里使用GoTo是可以的;它有效。这个问题不适合Programmers-SE,因为它是一个特定于.NET Framework、HPC计算和VB.Net语言上下文中提出的问题,询问是否存在更好的算法形式。 - Rob Perkins
话虽如此,请保留“可能重复”的标记,以帮助其他用户找到这些问题的答案。 - Rob Perkins
@RobPerkins:我真的认为这适合程序员。说实话,这不是关于.NET的问题,而更多地涉及到有效算法设计和性能考虑。你可以用C、Java或Python问同样的问题,答案都是一样的。 - siride
你可以这样做,但我必须考虑到.NET JITter,它是这个问题的一个组成部分。 - Rob Perkins
显示剩余3条评论
4个回答

5

有没有理由不将其推出到一个独立的方法中,然后使用 MethodImplOptions.AggressiveInlining ("如果可能应内联该方法") 进行装饰。

只要该方法满足某些要求(见下文),编译器就会在调用该方法的地方创建该方法的副本。

这样你就可以使用 Return 并大大简化代码,同时跳过通常与方法调用相关的堆栈推送、跳转等操作。

不幸的是,在您强制执行的约束条件下,几乎没有其他选择。

按要求,在 VB.Net 中提供一些示例用法:

Imports System.Runtime.CompilerServices

<MethodImpl(MethodImplOptions.AggressiveInlining)>
Public Function Blah() As String 
    ...
End Function

和C#

using System.Runtime.CompilerServices;

[MethodImpl(MethodImplOptions.AggressiveInlining)] 
public string Blah() {
    ...
}

我应该提到这是对编译器的提示,因此存在一些限制。以下不支持内联:
  • 虚拟方法
  • 递归方法
  • 将大型值类型作为参数的方法
  • MarshalByRef类上的方法
  • 具有复杂流程图的方法
  • 满足其他更奇特标准的方法
还可能存在IL字节计数限制(没有此标志的限制为32字节,该限制可能会增加或完全删除)。我无法找到充分的文档资料。

基本上,我能想到的唯一原因就是我不知道 MethodImplOptions.AggressiveInlining 这个方法。我会尝试一下! - Rob Perkins
你是否愿意编辑你的答案,包括一个带有适当修饰的C#或VB.Net函数头? - Rob Perkins
@RobPerkins 例子有点晚了,但我希望这能帮到你。 - Basic
成功了!现在的计时测试在GoTo变体和有分离方法的变体之间变化,有时候一个更快,有时候另一个更快。这表明编译器至少做得和手动优化一样好,而计时方面的差异可以通过其他方式解释。 - Rob Perkins
不错!当你获得这样的胜利时总是令人满意。很高兴我能帮忙 - 再见。 - Basic

1

虽然我建议您使用分离的方法来执行循环,但如果您真的想使用嵌套循环,有一种从您喜欢的循环中跳出的替代方法:

    Dim list1 = Enumerable.Range(1, 10)
    Dim list2 = Enumerable.Range(101, 10)
    Dim list3 = Enumerable.Range(201, 10)

    Console.WriteLine("Loop Start")

    For i As Integer = 0 To list1.Count - 1
        For j As Integer = 0 To list2.Count - 1
            For k As Integer = 0 To list3.Count - 1
                Console.WriteLine(k)
                If list3(k) = 205 Then ' Assume this is the condition to exit
                    k = list3.Count ' -- exit the loop of list3
                    j = list2.Count ' -- exit the loop of list2
                End If
            Next
        Next
    Next

    Console.WriteLine("Finished")

我写了一个更简单的示例(不使用您的复杂示例),这可以与嵌套循环一起使用(无论循环数量如何)。 我相信开销将是最小的。

这看起来比“GoTo”还要丑陋,我认为。 - svick
确切地说,这就是为什么我会说使用分离的方法更好。 - Rex

1
简单 - 将这段代码 For m1 = m - 1 To m + 1 改写成一个 While 循环。然后使用 Exit While。你可以像这样处理多个 GoTo,因为还有一个带有自己的 Exit DoDo 循环。有关 MSDN 上 Exit 语句的更多信息
虽然,我更喜欢将其重构为以下形式:
Sub SomeLambda(m As Integer, newarray As Short(,,))
  For n = 0 To ny
    For o = 0 To nz
      If newarray(m, n, o) = 1 OrElse newarray(m, n, o) = -1 Then Continue For
      DoSomething(m, n, o, newarray)
    Next
  Next
End Sub

Private Sub DoSomething(m, n, o, newarray)
  For m1 = m - 1 To m + 1
    For n1 = n - 1 To n + 1
      For o1 = o - 1 To o + 1
        If Not SomeCondition() = True Then Continue For 'the array is not out of bounds 
        Dim testVal = newarray(m1, n1, o1)
        If testVal <> -1 Then Continue For
        newarray(m, n, o) = -2
        Return
      Next
    Next
  Next
End Sub

确保它不会影响性能,始终使用Option Strict On。上面的代码显然不能编译 - 只是为了展示概念。
请注意,我删除了不必要的缩进,使代码变得更加扁平化和易读。 编辑:在性能和可维护性之间做出妥协,您可以使用类级别变量。因此,是的,您仍然会跳转到DoSomething,但没有堆栈推送。

0
我的愿望是避免在最内层循环中包含 callicall 操作码的 IL 编译。
你不应该太关心代码生成的 IL,对性能来说唯一重要的是机器码(通常为 x86)。
因此,你应该用可读性好的方式编写代码(使用另一个带有 Return 的函数来替代内部循环),然后测试性能,看它是否有区别。
作为替代方案,你可以查看编译后的机器代码,看看内部循环方法是否被内联。

当然,作为一名软件开发的20年老手,我对这种方法论并不陌生,因为我使用了它,包括RedGate分析器,来得出我所拥有的函数。 - Rob Perkins
@RobPerkins,所以您的意思是您测量了两个版本,使用另一种方法的那个实际上比较慢?因为我没有看到您在任何地方提到这一点,我认为这是一个重要的信息。 - svick
1
@RobPerkins,那你为什么不与我们分享相关的结果呢? - svick
因为这与问题无关,问题是如何删除一个GoTo语句而不会在结果IL中出现callicall。很明显,如果IL没有这些操作码,那么jitted x64中也不会有call。 毕竟,该算法的时间复杂度为O(n^3 + 9n)... - Rob Perkins
@RobPerkins 如果您有如此丰富的经验,那么您肯定知道XY问题,并且我们无法识别它是否适用于此除非您与我们分享您所知道的内容 - svick

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