Copy/PasteSpecial vs Range.Value = Range.Value

Dan*_*ken 5 excel vba

我已经在这个网站(以及其他地方)多次阅读过,如果可能的话,最好避免在VBA宏中复制/粘贴.例如,而不是这样做......

For i = 1 To tbl.ListColumns.Count
    With tbl.ListColumns(i).DataBodyRange
        .FormulaR1C1 = "=2*1"
        .Copy
        .PasteSpecial Paste:=xlPasteValues
        Application.CutCopyMode = False
    End With
Next
Run Code Online (Sandbox Code Playgroud)

......据说这样做更好/更快:

For i = 1 To tbl.ListColumns.Count
    With tbl.ListColumns(i)
        .DataBodyRange.FormulaR1C1 = "=2*1"
        .DataBodyRange = .DataBodyRange.Value
    End With
Next
Run Code Online (Sandbox Code Playgroud)

但是在大表(15列,100k行)上测试它,复制/粘贴版本明显更快(1.9秒对2.7秒).即使我首先将tbl.DataBodyRange声明为Range变量,差异仍然存在.

我认为这可能是ListObjects的一些奇怪属性,但如果没有它们,差异实际上更大:

'Runs in 1.1 seconds
With Sheet1.Range("A1:O100000")
    .FormulaR1C1 = "=2*1"
    .Copy
    .PasteSpecial Paste:=xlPasteValues
    Application.CutCopyMode = False
End With

'Runs in 2.1 seconds
With Sheet1.Range("A1:O100000")
    .FormulaR1C1 = "=2*1"
    .Value = .Value
End With
Run Code Online (Sandbox Code Playgroud)

有谁知道为什么复制/粘贴方法更快?是否有其他理由避免使用复制/粘贴(假设在宏运行时剪贴板永远不会在Excel之外使用)?

编辑:这是第一组测试结果,比较了Copy/PasteValues与Mat's Mug在接受的答案中描述的数组读/写方法.我测试了1000个细胞到100万个细胞的范围大小,每次增加1000个,并且每个范围大小平均进行10次测试.复制粘贴开始较慢,但很快超过了设定值方法(在图表上很难看到,但收支平衡点是~15k单元格).

完整的测试结果

我还在该范围的下端进行了10次进一步的测试(范围从100个单元到100000个单元,每次增加100个)以试图确定发生均衡点的位置.这次我使用Charles Williams的"MicroTimer"而不是默认计时器,希望它能够在亚秒级时间内更准确.我还包括"Set Array"版本和原始的".Value = .Value"版本(记住将计算切换到Manual,与第一组测试不同).有趣的是,此次阵列读/写方法的表现明显更差,大约3300个单元的均衡点和更差的峰值性能.数组读/写和.Value = .Value之间几乎没有区别,尽管阵列版本表现稍差.

完整测试2结果

这是我用于最后一轮测试的代码:

Sub speedTest()
    Dim copyPasteRNG(1 To 10, 1 To 1000)
    Dim setValueRNG(1 To 10, 1 To 1000)
    Dim setValueArrRNG(1 To 10, 1 To 1000)

    Dim i As Long
    Dim j As Long
    Dim numRows As Long
    Dim rng As Range

    Application.ScreenUpdating = False
    Application.Calculation = xlCalculationManual
    Application.DisplayStatusBar = False

    For i = 1 To 10
        numRows = 100
        For j = 1 To 1000
            Set rng = Sheet3.Range("A1:A" & numRows)
            setValueRNG(i, j) = getTime(False, rng, False)
            setValueArrRNG(i, j) = getTime(False, rng, True)
            numRows = numRows + 100
        Next
    Next

    For i = 1 To 10
        numRows = 100
        For j = 1 To 1000
            Set rng = Sheet3.Range("A1:A" & numRows)
            copyPasteRNG(i, j) = getTime(True, rng)
            numRows = numRows + 100
        Next
    Next

    Sheet4.Range("A1:J1000").Value2 = Application.Transpose(copyPasteRNG)
    Sheet5.Range("A1:J1000").Value2 = Application.Transpose(setValueRNG)

    Application.DisplayStatusBar = True
    Application.Calculation = xlCalculationAutomatic
    Application.ScreenUpdating = True

End Sub

Function getTime(copyPaste As Boolean, rng As Range, Optional arrB As Boolean) As Double
    Dim startTime As Double
    Dim endTime As Double

    startTime = MicroTimer

    With rng
        .FormulaR1C1 = "=1"
        If copyPaste = True Then
            .Copy
            .PasteSpecial Paste:=xlPasteValues
            Application.CutCopyMode = False
        ElseIf arrB = True Then
            Dim arr As Variant
            arr = .Value2
            .Value2 = arr
        Else
            .Value2 = .Value2
        End If
    End With

    endTime = MicroTimer - startTime

    getTime = endTime

End Function
Run Code Online (Sandbox Code Playgroud)

这是我使用的MicroTimer版本(在单独的模块中):

Private Declare PtrSafe Function getFrequency Lib "kernel32" Alias "QueryPerformanceFrequency" (cyFrequency As Currency) As Long
Private Declare PtrSafe Function getTickCount Lib "kernel32" Alias "QueryPerformanceCounter" (cyTickCount As Currency) As Long

Private Const sCPURegKey = "HARDWARE\DESCRIPTION\System\CentralProcessor\0"
Private Const HKEY_LOCAL_MACHINE As Long = &H80000002
Private Declare PtrSafe Function RegCloseKey Lib "advapi32.dll" (ByVal hKey As Long) As Long
Private Declare PtrSafe Function RegOpenKey Lib "advapi32.dll" Alias "RegOpenKeyA" (ByVal hKey As Long, ByVal lpSubKey As String, phkResult As Long) As Long
Private Declare PtrSafe Function RegQueryValueEx Lib "advapi32.dll" Alias "RegQueryValueExA" (ByVal hKey As Long, ByVal lpValueName As String, ByVal lpReserved As Long, lpType As Long, lpData As Any, lpcbData As Long) As Long

Function MicroTimer() As Double

    Dim cyTicks1 As Currency
    Static cyFrequency As Currency
     '
    MicroTimer = 0
    If cyFrequency = 0 Then getFrequency cyFrequency
    getTickCount cyTicks1
    If cyFrequency Then MicroTimer = cyTicks1 / cyFrequency

End Function
Run Code Online (Sandbox Code Playgroud)

Mat*_*don 6

大多数(很多,无论如何)VBA宏不"使用集合"并迭代一个范围内的单元格.不是因为这是一个好主意(事实并非如此),而是因为很多人根本不知道更好.

使用诸如a之类的对象集合时,最快的循环RangeFor Each循环.所以我接受了你的测试,稍微重构了一下,添加了迭代解决方案的测试,然后我添加了一个数组读/写测试,因为这也是复制单元格值的常用,好方法.

请注意,我从单独的测试中提取了公式编写设置步骤.

注意:此代码采用控制流程最佳实践并将它们推到地毯下.不要在实际代码中使用GoSub/ Return.

Sub Test()

    Const TEST_ROWCOUNT As Long = 10

    Const RANGE_ADDRESS As String = "A1:O" & TEST_ROWCOUNT
    Const RANGE_FORMULA As String = "=2*1"

    Dim startTime As Double

    Application.ScreenUpdating = False
    Application.Calculation = xlCalculationManual

    Debug.Print "Testing with " & Sheet1.Range(RANGE_ADDRESS).Count & " cells (" & TEST_ROWCOUNT & " rows)"

    GoSub InitTimer
    TestPasteFromClipboard Sheet1.Range(RANGE_ADDRESS)
    Debug.Print "Pasting from clipboard, single operation:",
    GoSub ReportTime

    GoSub InitTimer
    TestSetRangeValue Sheet1.Range(RANGE_ADDRESS)
    Debug.Print "Setting cell values, single operation:",
    GoSub ReportTime

    GoSub InitTimer
    TestIteratePaste Sheet1.Range(RANGE_ADDRESS)
    Debug.Print "Pasting from clipboard, iterative:",
    GoSub ReportTime

    GoSub InitTimer
    TestIterateSetValue Sheet1.Range(RANGE_ADDRESS)
    Debug.Print "Setting cell values, iterative:",
    GoSub ReportTime

    Application.ScreenUpdating = True
    Application.Calculation = xlCalculationAutomatic

    Exit Sub

InitTimer:
    Sheet1.Range(RANGE_ADDRESS).Formula = RANGE_FORMULA
    startTime = Timer
    Return
ReportTime:
    Debug.Print (Timer - startTime) * 1000 & "ms"
    Return
End Sub

Private Sub TestPasteFromClipboard(ByVal withRange As Range)
    With withRange
        .Copy
        .PasteSpecial Paste:=xlPasteValues
    End With
    Application.CutCopyMode = False
End Sub

Private Sub TestSetRangeValue(ByVal withRange As Range)
    withRange.Value = withRange.Value
End Sub

Private Sub TestIteratePaste(ByVal withRange As Range)
    Dim cell As Range
    For Each cell In withRange.Cells
        cell.Copy
        cell.PasteSpecial Paste:=xlPasteValues
    Next
    Application.CutCopyMode = False
End Sub

Private Sub TestIterateSetValue(ByVal withRange As Range)
    Dim cell As Range
    For Each cell In withRange.Cells
        cell.Value = cell.Value
    Next
    Application.CutCopyMode = False
End Sub
Run Code Online (Sandbox Code Playgroud)

我不得不将范围大小缩小一个数量级(否则我仍然会盯着我的无响应的Excel屏幕),但这是输出 - 当然逐个细胞的迭代方法要慢很多,但是注意剪贴板数字与直接Value分配的比较:

Testing with 150 cells (10 rows)
Pasting from clipboard, single operation: 11.71875ms
Setting cell values, single operation:    3.90625ms
Pasting from clipboard, iterative:        1773.4375ms
Setting cell values, iterative:           105.46875ms

Testing with 150 cells (10 rows)
Pasting from clipboard, single operation: 11.71875ms
Setting cell values, single operation:    3.90625ms
Pasting from clipboard, iterative:        1718.75ms
Setting cell values, iterative:           109.375ms

Testing with 150 cells (10 rows)
Pasting from clipboard, single operation: 15.625ms
Setting cell values, single operation:    3.90625ms
Pasting from clipboard, iterative:        1691.40625ms
Setting cell values, iterative:           136.71875ms
Run Code Online (Sandbox Code Playgroud)

因此,对于10行/ 150个单元格,将范围复制到数组/分配Range.Value比剪贴板解决方案快得多.

显然,迭代方法要慢得多,但请注意剪贴板解决方案与直接分配范围值相比多少!


是时候进行另一次测试了.

Testing with 1500 cells (100 rows)
Pasting from clipboard, single operation: 11.71875ms
Setting cell values, single operation:    7.8125ms
Pasting from clipboard, iterative:        10480.46875ms
Setting cell values, iterative:           1125ms

Testing with 1500 cells (100 rows)
Pasting from clipboard, single operation: 19.53125ms
Setting cell values, single operation:    3.90625ms
Pasting from clipboard, iterative:        10859.375ms
Setting cell values, iterative:           2390.625ms

Testing with 1500 cells (100 rows)
Pasting from clipboard, single operation: 15.625ms
Setting cell values, single operation:    3.90625ms
Pasting from clipboard, iterative:        10964.84375ms
Setting cell values, iterative:           1062.5ms
Run Code Online (Sandbox Code Playgroud)

现在更不那么明确,但倾销阵列似乎仍然是更可靠更快的解决方案.


让我们看看1000行给了我们:

Testing with 15000 cells (1000 rows)
Pasting from clipboard, single operation: 15.625ms
Setting cell values, single operation:    15.625ms
Pasting from clipboard, iterative:        80324.21875ms
Setting cell values, iterative:           11859.375ms
Run Code Online (Sandbox Code Playgroud)

我没有耐心.评论迭代测试.

Testing with 15000 cells (1000 rows)
Pasting from clipboard, single operation: 19.53125ms
Setting cell values, single operation:    15.625ms

Testing with 15000 cells (1000 rows)
Pasting from clipboard, single operation: 23.4375ms
Setting cell values, single operation:    15.625ms
Run Code Online (Sandbox Code Playgroud)

非常一致; 再次,剪贴板丢失.但是10K行怎么样?

Testing with 150000 cells (10000 rows)
Pasting from clipboard, single operation: 46.875ms
Setting cell values, single operation:    144.53125ms

Testing with 150000 cells (10000 rows)
Pasting from clipboard, single operation: 46.875ms
Setting cell values, single operation:    148.4375ms

Testing with 150000 cells (10000 rows)
Pasting from clipboard, single operation: 50.78125ms
Setting cell values, single operation:    144.53125ms
Run Code Online (Sandbox Code Playgroud)

我们在这里 - 剪贴板现在明显赢了!


底线:如果您有100K单元可以使用,剪贴板可能是一个好主意.如果你有10K单元可以使用(或更少),Value分配 数组转储可能是更快的方法.中间的任何内容可能需要基准测试和测试才能找出更快的方法.

TL; DR:没有银弹一刀切的解决方案.

当您使用相对较少数量的单元格时,和/或如果您正在迭代单个单元格时,您将希望避免复制/粘贴.对于涉及大量数据的大型批量操作,剪贴板并不是一个疯狂的想法.

为了完成:

Testing with 1500000 cells (100000 rows)
Pasting from clipboard, single operation: 324.21875ms
Setting cell values, single operation:    1496.09375ms

Testing with 1500000 cells (100000 rows)
Pasting from clipboard, single operation: 324.21875ms
Setting cell values, single operation:    1445.3125ms

Testing with 1500000 cells (100000 rows)
Pasting from clipboard, single operation: 367.1875ms
Setting cell values, single operation:    1562.5ms
Run Code Online (Sandbox Code Playgroud)

对于巨大的 YUGE范围,直接设置单元格值似乎始终优于阵列转储,但剪贴板优于两者,并且相当大.

所以:

  • 小于100K的单元格:数组转储/值分配
  • 超过150K的细胞:剪贴板
  • 介于两者之间:数组转储或剪贴板,测试找出
  • 在任何情况下,更快的方法都是迭代解决方案,几个数量级.

  • @DanielMcCracken你知道,现在我很想接受这些测试并将它们实现为[Rubberduck](http://rubberduckvba.com)单元测试,只是为了满足我的OCD,编写的代码使用`GoSub` /`Return `. (2认同)
  • 理论上,使用"Value2"应该比"Value"更快.例如.`withRange.Value2 = withRange.Value2` (2认同)