【问题标题】:Improving the performance of FOR loop提高 FOR 循环的性能
【发布时间】:2016-01-02 07:04:35
【问题描述】:

我正在比较工作簿中的工作表。该工作簿有两张名为 PRE 和 POST 的工作表,每张都有相同的 19 列。行数每天都在变化,但在特定日期的两张纸上是相同的。该宏将 PRE 表中的每一行与 POST 表中的相应行进行比较,如果两个表中的行相同,则删除它们。

我有通常建议的提高性能的方法,例如将屏幕更新设置为 FALSE 等。

我想优化两个FOR NEXT 循环。

Dim RESULT As String

iPRE = ActiveWorkbook.Worksheets("PRE").Range("A1", Worksheets("PRE").Range("A1").End(xlDown)).Rows.Count
'MsgBox iPRE
iPOST = ActiveWorkbook.Worksheets("POST").Range("A1", Worksheets("POST").Range("A1").End(xlDown)).Rows.Count
'MsgBox iPOST

If iPRE <> iPOST Then
    MsgBox "The number of rows in PRE and POST sheets are not the same. The macro quits"
    Exit Sub

Else
    iRows = iPRE
End If

 'Optimize Performance

    Application.ScreenUpdating = False

    EventState = Application.EnableEvents
    Application.EnableEvents = False

    CalcState = Application.Calculation
    Application.Calculation = xlCalculationManual

    PageBreakState = ActiveSheet.DisplayPageBreaks
    ActiveSheet.DisplayPageBreaks = False

    For iCntr = iRows To 2 Step -1
        For y = 1 To 20
            If Worksheets("PRE").Cells(iCntr, y) <> Worksheets("POST").Cells(iCntr, y) Then
                RESULT = "DeleteN"
                Exit For
            Else
                RESULT = "DeleteY"
            End If
        Next y

        If RESULT = "DeleteY" Then
            Worksheets("PRE").Rows(iCntr).Delete
            Worksheets("POST").Rows(iCntr).Delete
        End If
    Next iCntr

    'Revert optmizing lines

    ActiveSheet.DisplayPageBreaks = PageBreakState
    Application.Calculation = CalcState
    Application.EnableEvents = EventState
    Application.ScreenUpdating = True

End Sub

【问题讨论】:

  • Deleting Rows (Row by Row) 很慢,尝试使用Union 并删除所有Rows 一次,例如如果您的宏将删除1000 行,使用Union 将是1000 次更快,但如果你想删除 1 或 2 行,此方法将无济于事。

标签: performance excel vba


【解决方案1】:

对工作表单元格的任何引用都很慢。当您在循环中执行此操作时,这会显着增加。最好的速度提升将来自于限制这些工作表引用。

一种好方法是复制 Variant Arrays 中的数据,然后循环遍历这些数据,构建一个包含要保留的数据的新 Variant Array。然后将新的数组一次性的放在旧的数组上。

使用 200,000 行、20 列、50% 文本、50% 数字的测试数据集,删除 170,000 行:此代码在我的硬件上运行大约 30 秒

Sub Mine2()
    Dim T1 As Long, T2 As Long, T3 As Long

    Dim ResDelete As Boolean
    Dim iPRE As Long, iPOST As Long
    Dim EventState  As Boolean, CalcState As XlCalculation, PageBreakState As Boolean
    Dim iCntr As Long, y As Long, iRows As Long
    Dim rPre As Range, rPost As Range

    Dim PreDat As Variant, PostDat As Variant, PreDelDat As Variant, PostDelDat As Variant

    Dim n As Long
    Dim wsPre As Worksheet, wsPost As Worksheet

    Set wsPre = ActiveWorkbook.Worksheets("PRE")
    With wsPre
        Set rPre = .Range(.Cells(1, .Columns.Count).End(xlToLeft), .Cells(.Rows.Count, 1).End(xlUp))
        PreDat = rPre.Value
        iPRE = UBound(PreDat, 1)
        'MsgBox iPRE
    End With

    Set wsPost = ActiveWorkbook.Worksheets("POST")
    With wsPost
        Set rPost = .Range(.Cells(1, .Columns.Count).End(xlToLeft), .Cells(.Rows.Count, 1).End(xlUp))
        PostDat = rPost.Value
        iPOST = UBound(PostDat, 1)
        'MsgBox iPOST
    End With

    If iPRE <> iPOST Then
        MsgBox "The number of rows in PRE and POST sheets are not the same. The macro quits"
        Exit Sub
    End If
    iRows = iPRE


    ReDim PreDelDat(1 To UBound(PreDat, 1), 1 To UBound(PreDat, 2))
    ReDim PostDelDat(1 To UBound(PostDat, 1), 1 To UBound(PostDat, 2))
    n = 1
    On Error GoTo EH:
 'Optimize Performance

    Application.ScreenUpdating = False
    EventState = Application.EnableEvents
    Application.EnableEvents = False

    CalcState = Application.Calculation
    Application.Calculation = xlCalculationManual

    PageBreakState = ActiveSheet.DisplayPageBreaks
    ActiveSheet.DisplayPageBreaks = False


    T1 = GetTickCount
    For y = 1 To UBound(PreDat, 2)
        PreDelDat(1, y) = PreDat(1, y)
        PostDelDat(1, y) = PostDat(1, y)
    Next

    n = 2
    For iCntr = 2 To UBound(PreDat, 1)
        ResDelete = True
        For y = 1 To UBound(PreDat, 2)
            If PreDat(iCntr, y) <> PostDat(iCntr, y) Then
                ResDelete = False
                Exit For
            End If
        Next y

        If Not ResDelete Then
            For y = 1 To UBound(PreDat, 2)
                PreDelDat(n, y) = PreDat(iCntr, y)
                PostDelDat(n, y) = PostDat(iCntr, y)
            Next
            n = n + 1
        End If
    Next iCntr
    T2 = GetTickCount
    Debug.Print "Compare Done in:", T2 - T1
    Debug.Print "Rows to delete:", n - 1

    rPre = PreDelDat
    rPost = PostDelDat

    T3 = GetTickCount
    Debug.Print "Delete Done In:", T3 - T1
CleanUp:
    'Revert optmizing lines
    On Error Resume Next
    ActiveSheet.DisplayPageBreaks = PageBreakState
    Application.Calculation = CalcState
    Application.EnableEvents = EventState
    Application.ScreenUpdating = True
Exit Sub
EH:
    ' Handle Errors here
    Debug.Assert False
    Resume
    Err.Clear
    Resume CleanUp
End Sub

原文:

一种好方法是复制变量数组中的数据,然后循环遍历这些数据,构建对单元格的引用以便稍后删除。然后一次性删除。

其他一般提示:

  • 声明所有变量
  • 使用更合适的数据类型(Long、Boolean)
  • 使用End(xlUp) 避免在意外空白处失败(除非您想要在第一个空白处停止)

重构代码:

Sub Demo()
    Dim ResDelete As Boolean
    Dim iPRE As Long, iPOST As Long
    Dim EventState  As Boolean, CalcState As XlCalculation, PageBreakState As Boolean
    Dim iCntr As Long, y As Long, iRows As Long
    Dim rPreDelete As Range, rPostDelete As Range

    Dim PreDat As Variant, PostDat As Variant

    With ActiveWorkbook.Worksheets("PRE")
        PreDat = .Range(.Cells(1, 20), .Cells(.Rows.Count, 1).End(xlUp)).Value
        iPRE = UBound(PreDat, 1)
        'MsgBox iPRE
    End With

    With ActiveWorkbook.Worksheets("POST")
        PostDat = .Range(.Cells(1, 20), .Cells(.Rows.Count, 1).End(xlUp)).Value
        iPOST = UBound(PostDat, 1)
        'MsgBox iPOST
    End With

    If iPRE <> iPOST Then
        MsgBox "The number of rows in PRE and POST sheets are not the same. The macro quits"
        Exit Sub
    End If
    iRows = iPRE

    On Error GoTo EH:
 'Optimize Performance

    Application.ScreenUpdating = False
    EventState = Application.EnableEvents
    Application.EnableEvents = False

    CalcState = Application.Calculation
    Application.Calculation = xlCalculationManual

    PageBreakState = ActiveSheet.DisplayPageBreaks
    ActiveSheet.DisplayPageBreaks = False

    For iCntr = 2 To UBound(PreDat, 1)
        ResDelete = True
        For y = 1 To 20
            If PreDat(iCntr, y) <> PostDat(iCntr, y) Then
                ResDelete = False
                Exit For
            End If
        Next y

        If ResDelete Then
            If rPreDelete Is Nothing Then
                Set rPreDelete = Worksheets("PRE").Rows(iCntr)
                Set rPostDelete = Worksheets("POST").Rows(iCntr)
            Else
                Set rPreDelete = Application.Union(rPreDelete, Worksheets("PRE").Rows(iCntr))
                Set rPostDelete = Application.Union(rPostDelete, Worksheets("POST").Rows(iCntr))
            End If
        End If
    Next iCntr
    If Not rPreDelete Is Nothing Then
        rPreDelete.Delete
        rPostDelete.Delete
    End If

CleanUp:
    'Revert optmizing lines
    On Error Resume Next
    ActiveSheet.DisplayPageBreaks = PageBreakState
    Application.Calculation = CalcState
    Application.EnableEvents = EventState
    Application.ScreenUpdating = True
Exit Sub
EH:
    ' Handle Errors here

    Resume CleanUp
End Sub

【讨论】:

  • 非常感谢克里斯的帮助。我确实尝试了代码,但由于一些内存问题,最初它抛出了运行时错误 7。它在执行以下行时显示错误 PostDat = .Range(.Cells(1, 20), .Cells(.Rows.Count, 1).End(xlUp)).Value。我重新启动机器并再次尝试,但这次 Excel 挂起,即使放置一夜也无法恢复。你知道为什么吗?一个原因当然可能是我的数据有 210 万行。还有什么可以做的吗?我还将尝试 SilentRevolution 提供的代码,但正如您所说,它的功能与您的代码相似
  • @TamalBose 我已经测试了代码没有问题。 (250,000 行随机数据)
  • 我还可以在这里补充一下,我的原始代码当然效率不高,但能够在大约 4 小时内完成工作
  • 这段代码比你的快多少取决于你的数据集。我实现了与 SilentRevolution 发布的类似改进:250,000 行的 8.5 秒 vs 24.7 秒,删除了 500 行。你能描述一下你的数据吗? (初始行总数?,删除的行?数据中有公式吗?)
  • @Chris Nielsen,我的数据就像每张纸中的 200,000 行,20 列,删除的行将根据 Pre 和 Post 之间的应用程序变化而有所不同,但可以假设一个球场数为 170,000 .数据中没有公式。我的原始代码在大约 4 小时内运行,因此将其降低到一半将是一个相当大的成就
【解决方案2】:

如果我可以投入两分钱,这是我的建议。

我已经测试了原始代码(唯一的更改是 For y = 1 to 10 而不是 For y = 1 to 20)和我的代码针对 2 张 10 列和(最初 500,000)250,000 行数据的工作表。我使用 10 而不是 20 的原因在于我不知道列中的数据是什么,因此我使用了 1 或 2 的随机值。

  • 对于 10 列,这意味着有 2^10 = 1,024 的可能性。
  • 对于 20 列,这意味着有 2^20 = 1,048,576 的可能性。

因为我希望每个表中至少有几行相同的可能性,所以我选择了 10 列方案。

为了给宏计时,我设置了一个定时器宏,它调用宏来比较和删除数据。

为了能够比较结果,两个宏都是在启动 Excel 并打开具有完全相同数据的文件后直接执行的。

我有

  • 避免Active的所有实例
  • 最大限度地减少 Excel 和 VBA 之间的数据读取和写入,这是通过将工作表上的所有数据收集到二维数组中然后分析数组来完成的。
  • 收集范围内要删除的行(每张 1 个)并删除循环外要删除的所有行

代码

Sub CompareAndDelete()
    Dim WsPre As Worksheet, WsPost As Worksheet
    Dim Row As Long, Column As Long
    Dim ArrPre() As Variant, ArrPost() As Variant
    Dim DeleteRow As Boolean
    Dim DeletePre As Range, DeletePost As Range

    With Application
        .ScreenUpdating = False
        .Calculation = xlCalculationManual
        .EnableEvents = False
    End With

    With ThisWorkbook
        Set WsPre = .Worksheets("PRE")
        Set WsPost = .Worksheets("Post")
    End With

    ArrPre = WsPre.Range(WsPre.Cells(1, 1), WsPre.Cells(WsPre.Cells(WsPre.Rows.Count, 1).End(xlUp).Row, 20))
    ArrPost = WsPost.Range(WsPost.Cells(1, 1), WsPost.Cells(WsPost.Cells(WsPost.Rows.Count, 1).End(xlUp).Row, 20))

    If Not UBound(ArrPre, 1) = UBound(ArrPost, 1) Then
        MsgBox "Unequal number of rows in sheets PRE and POST. Exiting macro.", vbCritical, "Unequal sheets"
    Else

        For Row = 2 To UBound(ArrPre, 1)
            DeleteRow = True
            For Column = 1 To UBound(ArrPre, 2)
                If Not ArrPre(Row, Column) = ArrPost(Row, Column) Then
                    DeleteRow = False
                    Exit For
                End If
            Next Column

            If DeleteRow = True Then
                If DeletePre Is Nothing Then
                    Set DeletePre = WsPre.Rows(Row)
                    Set DeletePost = WsPost.Rows(Row)
                Else
                    Set DeletePre = Union(DeletePre, WsPre.Rows(Row))
                    Set DeletePost = Union(DeletePost, WsPost.Rows(Row))
                End If

            End If
        Next Row

        If Not DeletePre Is Nothing Then DeletePre.Delete
        If Not DeletePost Is Nothing Then DeletePost.Delete

    End If

    With Application
        .ScreenUpdating = True
        .Calculation = xlCalculationAutomatic
        .EnableEvents = True
    End With

End Sub

结果

我的代码 - 500,000 行数据。

已在 14.23 秒内处理了 500.000 行和 10 列的数据表,发现 561 行相等并已被删除。

原始代码 - 500,000 行数据。

很遗憾,我的系统无法处理此任务,Excel 停止工作。


我的代码 - 250,000 行数据。

已在 4.72 秒内处理了 250.000 行和 10 列的数据表,发现 313 行相等并已被删除。

原始代码 - 250,000 行数据。

已在 14.07 秒内处理了 250.000 行和 10 列的数据表,发现 313 行相等并已被删除。

【讨论】:

  • 这是与我的功能相同的代码。感谢测试它。
  • 看来你是对的@chrisneilsen。虽然没有注意到这一点。我正在研究它一段时间,最初有一个不同的设置,但它比原始代码效率低,所以我放弃了它,但仍然想找到一个解决方案,当它完成并测试时,你已经发布了你的。我仍然想展示结果。
【解决方案3】:

也许您可以进行 2 次调整,尽管它们对性能的影响很小:

' prepare references to worksheets
Dim WorksheetPRE As Worksheet
Dim WorksheetPOST As Worksheet
Set WorksheetPRE = ActiveWorkbook.Worksheets("PRE")
Set WorksheetPOST = ActiveWorkbook.Worksheets("POST")

然后,在您的代码中,将 ActiveWorkbook.Worksheets("PRE") 替换为 WorksheetPRE 等。

我认为,当您使用 Excel 时,不可能进行其他重大优化。请记住,Microsoft Excel 主要是spreadsheet 计算器,而不是数据表处理工具。

如果我真的需要加快比较速度,那么我会采用以下方法之一:

  • 将 Excel 工作表作为表格链接到 Microsoft Access 并在 Access 中执行比较(最简单)

  • 如上,但不是链接表,而是导入

  • 和上面两个一样,但是使用Microsoft SQL Server(Express版免费)

【讨论】:

    猜你喜欢
    • 2018-11-08
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-11-25
    • 2021-12-01
    相关资源
    最近更新 更多