Copy / PasteSpecial vs Range.Value = Range.Value

时间:2017-07-10 18:44:59

标签: 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

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

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

但是在大​​桌子(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

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

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

Full test results

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

Full test 2 results

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

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

这是我使用的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

1 个答案:

答案 0 :(得分:6)

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

使用对象集合(如Range)时,最快的循环是For 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

我不得不将范围大小缩小一个数量级(否则我仍然会盯着我的无响应的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

因此,对于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

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

让我们看看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

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

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

非常一致;再次,剪贴板丢失。但是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

我们在这里 - 剪贴板现在明显胜出!

底线:如果您要使用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

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

所以:

  • 少于100K的单元格:数组转储/值赋值
  • 150K以上的单元格:剪贴板
  • 介于两者之间:数组转储或剪贴板,测试以查找
  • 在任何情况下,更快的方法都是迭代解决方案,只需几个数量级。