HyperVの仮想マシンへのファイルコピー手段の速度比較

Hyper-Vの仮想マシンにファイルをコピーする時、方法によってどれくらいコピー時間が変わってくるのか気になったので計測しました。

※どれも一度だけ計測した結果で環境によっても違うかもしれないので参考程度です。

# 計測環境

- ホスト
	- OS: Windows 11 (Build 23615.1000)
	- CPU: Intel Core i7-7700K
	- RAM: DDR4-2133 32GB
	- SSD: Samsung 960EVO 500GB
- ゲスト
	- OS: Windows 11 (Build 22621.3007)
	- CPU: 4コア割り当て
	- RAM: 8GB割り当て

# 計測方法

PowerShellのスクリプト等を使って以下のA., B.の組み合わせについて確認した。なお、アーカイブの作成には`Compress-Archive -CompressionLevel NoCompression`を使用した。

- A. 中身が乱数の10GB分のファイルを以下の容量・数で生成する
    - 1GB x 10ファイル
    - 1MB x 10000ファイル
- B. コピーの方法
    - Copy-VHD
        - ホスト側で仮想マシンにコピーしたいファイルが入ったVHDを作成後、仮想マシンにVHDをマウントしてコピー
    - Copy-VMFile
        - ファイルを1つのファイルにアーカイブ後[^1]、`Copy-VMFile`を使ってコピー
    - Copy-Item
        - `Copy-Item`の`-ToSession`を使ってコピー
    - Copy-Item-Smb
        - `Copy-Item`を使って仮想マシンの共有フォルダにコピー
        - 仮想マシン側では事前に`C:\`を読み書き可能な形で共有しておく
    - Copy-Item-Compress
        - A.のファイルを1つのファイルにアーカイブ後、`Copy-Item`の`-ToSession`を使ってコピー
    - Copy-Item-Smb-Compress
        - A.のファイルを1つのファイルにアーカイブ後、`Copy-Item`を使って仮想マシンの共有フォルダにコピー

[^1]: `Copy-VMFile`ではフォルダのコピーができないため。

<details><summary>検証に使用したコード</summary>

テスト用データの生成 (※WSLで実行する想定です)
```sh:generate.sh
#!/bin/sh

mkdir 1x10000
(
	cd 1x10000
	for i in $(seq 1 10000); do
		dd if=/dev/urandom of=$i.bin bs=1M count=1
	done
)
mkdir 1000x10
(
	cd 1000x10
	for i in $(seq 1 10); do
		dd if=/dev/urandom of=$i.bin bs=1000M count=1
	done
)
```

速度計測スクリプト
他の環境で動かすことを想定していないので、想定しない動作をしないか確認してから実行してください。

```ps:bench.ps1
$vmname = "Windows 11"
$username = "User"
$password = "user"

$vm = Get-VM -Name $vmname
$networkAdapter = Get-VMNetworkAdapter -VM $vm
$ipAddresses = $networkAdapter.IPAddresses
$ipAddress = $ipAddresses[0]

$hostWork = "C:\work"
$guestWork = "C:\work"
$hostCompressTmp = "$hostWork\tmp.zip"
$guestCompressTmp = "$guestWork\tmp.zip"

$vhdPath = "C:\work\tmp.vhdx"
$vhdSize = 15GB

$password = ConvertTo-SecureString "$password" -AsPlainText -Force
$credential = New-Object System.Management.Automation.PSCredential ($username, $password)
$session = New-PSSession -VMName $vmname -Credential $credential

function Initialize-WorkingDirectory
{
    Invoke-Command -Session $session -ScriptBlock {
        if (Test-Path $using:guestWork) {
            Remove-Item -Path $using:guestWork -Recurse -Confirm:$false
        }
        New-Item -ItemType Directory -Path $using:guestWork > $null
    }
}

function Bench-Copy-Item($dir)
{
    $result = Measure-Command { Copy-Item -ToSession $session -Path $dir -Destination C:\work -Recurse }
    return @{
        "Result"=$result;
    }
}

function Bench-Copy-Item-Compress($dir)
{
    $result1 = Measure-Command { Prepare-Compress $dir }
    $result2 = Measure-Command { Copy-Item -ToSession $session -Path $hostCompressTmp -Destination $guestCompressTmp }
    $result3 = Measure-Command { Invoke-Command -Session $session -ScriptBlock { Expand-Archive -Path $using:guestCompressTmp -DestinationPath $using:guestWork } }
    Release-Compress
    return @{
        "Compress"=$result1;
        "Result"=$result2;
        "Decompress"=$result3;
    }
}

function Bench-Copy-VMFile($dir)
{
    $result1 = Measure-Command { Prepare-Compress $dir }
    $result2 = Measure-Command { Copy-VMFile $vmname -SourcePath $hostCompressTmp -DestinationPath $guestCompressTmp -CreateFullPath -FileSource Host }
    $result3 = Measure-Command { Invoke-Command -Session $session -ScriptBlock { Expand-Archive -Path $using:guestCompressTmp -DestinationPath $using:guestWork } }
    Release-Compress
    return @{
        "Compress"=$result1;
        "Result"=$result2;
        "Decompress"=$result3;
    }
}

function Bench-Copy-Item-Smb($dir)
{
    New-PSDrive -Name "Z" -PSProvider FileSystem -Root "\\$ipAddress\c" -Credential $credential > $null
    $result = Measure-Command { Copy-Item -Path $dir -Destination Z:\work -Recurse }
    Remove-PSDrive -Name "Z"
    return @{
        "Result"=$result;
    }
}

function Bench-Copy-Item-Smb-Compress($dir)
{
    New-PSDrive -Name "Z" -PSProvider FileSystem -Root "\\$ipAddress\c" -Credential $credential > $null
    $result1 = Measure-Command { Prepare-Compress $dir }
    $result2 = Measure-Command { Copy-Item -Path $hostCompressTmp -Destination Z:\work\tmp.zip }
    $result3 = Measure-Command { Invoke-Command -Session $session -ScriptBlock { Expand-Archive -Path $using:guestCompressTmp -DestinationPath $using:guestWork } }
    Release-Compress
    Remove-PSDrive -Name "Z"
    return @{
        "Compress"=$result1;
        "Result"=$result2;
        "Decompress"=$result3;
    }
}

function Bench-Copy-VHD($dir)
{
    if (Test-Path $vhdPath) {
        Remove-Item $vhdPath
    }
    $result1 = Measure-Command {
        New-VHD -Path $vhdPath -SizeBytes $vhdSize -Dynamic > $null
        $vhdx = Mount-VHD -Path $vhdPath -Passthru
        $disk = Initialize-Disk -Number $vhdx.Number -PassThru | New-Partition -AssignDriveLetter -UseMaximumSize | Format-Volume -NewFileSystemLabel "Bench"
        $driveLetter = ($disk | Get-Partition).DriveLetter

        Copy-Item -Path $dir -Destination "${driveLetter}:\" -Recurse

        Dismount-VHD -Path $vhdPath
    }
    $result2 = Measure-Command { Add-VMHardDiskDrive -VMName $vmName -Path $vhdPath }
    $result3 = Measure-Command {
        Invoke-Command -Session $session -ScriptBlock {
            $driveLetter = Get-Volume | Where-Object { $_.FileSystemLabel -eq "Bench" } | Select-Object -ExpandProperty DriveLetter
            Copy-Item -Path "${driveLetter}:\*" -Destination $using:guestWork -Recurse
        }
    }
    $result4 = Measure-Command { Get-VMHardDiskDrive -VMName $vmName | Where-Object { $_.Path -eq $vhdPath } | Remove-VMHardDiskDrive }
    return @{
        "Prepare"=$result1;
        "AttachVHD"=$result2;
        "Result"=$result3;
        "DetachVHD"=$result4;
    }
}

function Prepare-Compress($dir)
{
    if (Test-Path $hostCompressTmp) {
        Remove-Item -Path $hostCompressTmp -Recurse -Confirm:$false
    }
    Compress-Archive -Path $dir -DestinationPath $hostCompressTmp -Force -CompressionLevel NoCompression
}

function Release-Compress
{
    if (Test-Path $hostCompressTmp) {
        Remove-Item -Path $hostCompressTmp -Recurse -Confirm:$false
    }
}

$benchmarks = @{
    "Copy-Item"=${function:Bench-Copy-Item};
    "Copy-Item-Smb"=${function:Bench-Copy-Item-Smb};
    "Copy-VMFile"=${function:Bench-Copy-VMFile};
    "Copy-Item-Compress"=${function:Bench-Copy-Item-Compress};
    "Copy-Item-Smb-Compress"=${function:Bench-Copy-Item-Smb-Compress};
    "Copy-VHD"=${function:Bench-Copy-VHD};
}

$filePattern = @{
    "1000x10"="C:\work\1000x10";
    "1x10000"="C:\work\1x10000";
    #"test"="C:\work\test";
}

foreach($benchName in $benchmarks.Keys) {
    $func = $benchmarks[$benchName]
    
    foreach ($fileName in $filePattern.Keys) {
        $path = $filePattern[$fileName]

        Initialize-WorkingDirectory
        $results = & $func $path
        Write-Output "$benchName($fileName)"
        foreach ($resultName in $results.Keys) {
            $result = $results[$resultName]

            Write-Output "`t${resultName}: ${result}"
        }
    }
}
```
</details>

# 結果

|                        | 1GB x 10ファイル | 1MB x 10000ファイル |
|------------------------|-------------|-------------|
| Copy-VHD               | 00:01:26.22 | <font color="blue">00:01:18.84</font> |
| Copy-VMFile            | 00:06:12.45 | 00:07:33.64 |
| Copy-Item              | 00:19:19.66 | <font color="red">00:28:11.62</font> |
| Copy-Item-Smb          | <font color="blue">00:00:34.31</font> | 00:01:47.18 |
| Copy-Item-Compress     | <font color="red">00:21:12.26</font> | 00:23:54.06 |
| Copy-Item-Smb-Compress | 00:01:59.29 | 00:04:30.71 |

- コピーの方法と合計サイズが同じでも、大きいファイルが少し、小さいファイルが大量で比較すると、方法によっては最大数倍時間が変わってくることがある。 (Copy-Item-Smb)
- Copy-Item + 1MB x 10000ファイルのパターンでしか早くなっていないので、小さいファイルが大量にあったとしても基本的にはそのままコピーするのが早い。
- 今回の結果からすると、仮想マシンへのファイルコピーで一番良いのはWindowsの共有経由でコピー、次点でVHDファイルを使ってコピー[^2]が良さそう。

[^2]: VHDファイルを使うのであれば、わざわざ仮想マシン内にファイルをコピーせずにVHD内のファイルをそのまま読み書きをするという手もある。その場合、VHDの作成から仮想マシン内へのコピーにかかっているのが1分20秒前後で、後述の「VHDを作成する計測の詳細」からVHDの作成自体にかかっている時間は40秒程度なので、40秒程度でファイルの読み書きができる状態にできる。

## VHDを作成する計測の詳細

Copy-VHDでVHDの作成(VHDファイル自体の作成、フォーマット、VHDへのファイルのコピー)にかかった時間は以下の通り。

|                        | 1GB x 10ファイル | 1MB x 10000ファイル |
|-|-|-|
| Copy-VHD | 00:00:43.78 | 00:00:41.36 |

## アーカイブを作成する計測の詳細

アーカイブを作成してから仮想マシンへコピーする計測の詳細は以下の通り。

|                         | 作成 | ← | 展開 |← | コピーのみ |← |
|-------------------------|-------------|-------------|-------------|-------------|-------------|-------------|
|                         | 1GB x 10ファイル | 1MB x 10000ファイル | 1GB x 10ファイル | 1MB x 10000ファイル | 1GB x 10ファイル | 1MB x 10000ファイル |
| Copy-VMFile         | 00:00:54.63 | 00:03:05.60 | 00:01:40.63 | 00:01:20.90 | 00:03:37.18 | 00:03:07.14 |
| Copy-Item-Compress | 00:00:52.37 | 00:02:28.02 | 00:00:38.73 | 00:01:23.23 | 00:19:29.68 | 00:19:56.84 |
| Copy-Item-Smb-Compress | 00:00:53.81 | 00:02:36.31 | 00:00:48.77 | 00:01:20.91 | 00:00:28.18 | 00:00:39.45 |

- 同じ計測データを元にアーカイブを作成・展開しているのに、圧縮で30秒、展開で1分程度の違いが出ているので、やはり何度か計測した方が良さそう