为什么我的 C++ 磁盘写测试比使用 bash 的简单文件复制慢得多?
Why is my C++ disk write test much slower than a simply file copy using bash?
使用下面的程序,我尝试测试使用 std::ofstream
写入磁盘的速度。
我在写一个 1 GiB 的文件时达到了大约 300 MiB/s。
但是,使用 cp
命令进行简单的文件复制至少要快两倍。
我的程序是否达到了硬件限制或者是否可以提高速度?
#include <chrono>
#include <iostream>
#include <fstream>
char payload[1000 * 1000]; // 1 MB
void test(int MB)
{
// Configure buffer
char buffer[32 * 1000];
std::ofstream of("test.file");
of.rdbuf()->pubsetbuf(buffer, sizeof(buffer));
auto start_time = std::chrono::steady_clock::now();
// Write a total of 1 GB
for (auto i = 0; i != MB; ++i)
{
of.write(payload, sizeof(payload));
}
double elapsed_ns = std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::steady_clock::now() - start_time).count();
double megabytes_per_ns = 1e3 / elapsed_ns;
double megabytes_per_s = 1e9 * megabytes_per_ns;
std::cout << "Payload=" << MB << "MB Speed=" << megabytes_per_s << "MB/s" << std::endl;
}
int main()
{
for (auto i = 1; i <= 10; ++i)
{
test(i * 100);
}
}
输出:
Payload=100MB Speed=3792.06MB/s
Payload=200MB Speed=1790.41MB/s
Payload=300MB Speed=1204.66MB/s
Payload=400MB Speed=910.37MB/s
Payload=500MB Speed=722.704MB/s
Payload=600MB Speed=579.914MB/s
Payload=700MB Speed=499.281MB/s
Payload=800MB Speed=462.131MB/s
Payload=900MB Speed=411.414MB/s
Payload=1000MB Speed=364.613MB/s
更新
我从std::ofstream
变成了fwrite
:
#include <chrono>
#include <cstdio>
#include <iostream>
char payload[1024 * 1024]; // 1 MiB
void test(int number_of_megabytes)
{
FILE* file = fopen("test.file", "w");
auto start_time = std::chrono::steady_clock::now();
// Write a total of 1 GB
for (auto i = 0; i != number_of_megabytes; ++i)
{
fwrite(payload, 1, sizeof(payload), file );
}
fclose(file); // TODO: RAII
double elapsed_ns = std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::steady_clock::now() - start_time).count();
double megabytes_per_ns = 1e3 / elapsed_ns;
double megabytes_per_s = 1e9 * megabytes_per_ns;
std::cout << "Size=" << number_of_megabytes << "MiB Duration=" << long(0.5 + 100 * elapsed_ns/1e9)/100.0 << "s Speed=" << megabytes_per_s << "MiB/s" << std::endl;
}
int main()
{
test(256);
test(512);
test(1024);
test(1024);
}
将 1 GiB 文件的速度提高到 668MiB/s:
Size=256MiB Duration=0.4s Speed=2524.66MiB/s
Size=512MiB Duration=0.79s Speed=1262.41MiB/s
Size=1024MiB Duration=1.5s Speed=664.521MiB/s
Size=1024MiB Duration=1.5s Speed=668.85MiB/s
与 dd
:
一样快
time dd if=/dev/zero of=test.file bs=1024 count=0 seek=1048576
real 0m1.539s
user 0m0.001s
sys 0m0.344s
- Streams are slow
cp
直接使用系统调用 read(2)
或 mmap(2)
.
我敢打赌,它是 CP 或文件系统内部的一些聪明东西。如果它在 CP 中,则可能是您正在复制的文件中有很多 0,而 cp 正在检测到这一点并写入文件的 sparse 版本。 cp 的手册页说 "By default, sparse SOURCE files are detected by a crude heuristic and the corresponding DEST file is made sparse as well." 这可能意味着一些事情,但其中之一是 cp 可以制作文件的稀疏版本,这将需要更少的磁盘写入时间。
如果它在您的文件系统中,那么它可能是 Deduplication。
作为一个远景第三,它也可能是你的 OS 或你的磁盘固件中的某些东西,它正在将读写转换成一些不需要像你的程序那样同步的专用指令需要(较低的总线使用意味着较少的延迟)。
您使用的缓冲区大小相对较小。小缓冲区意味着每秒有更多的操作,这会增加开销。磁盘系统在收到 read/write 请求并开始处理之前会有少量延迟;更大的缓冲区摊销成本更好一些。较小的缓冲区也可能意味着磁盘花费更多的时间寻找。
您不会同时发出多个请求 - 您需要先完成一次读取,然后才能开始下一次读取。这意味着磁盘可能有死区时间,它什么都不做。由于所有写入都依赖于所有读取,并且您的读取是串行的,因此您正在使磁盘系统缺少读取请求(双重情况,因为写入会占用读取)。
所有读取请求的请求读取字节总数应大于磁盘系统的带宽延迟乘积。如果磁盘有 0.5 毫秒的延迟和 4 GB/sec 的性能,那么您希望始终有 4 GB * 0.5 毫秒 = 2 MB 的未完成读取。
您没有使用任何操作系统提示您正在执行顺序读取。
解决这个问题:
- 更改您的代码以始终有多个未完成的读取请求。
- 有足够的未完成读取请求,因此您正在等待至少 2 MB 的数据。
- 使用 posix_fadvise() 标志帮助 OS 磁盘调度和页面缓存优化。
- 考虑使用 mmap 来减少开销。
- 每个读取请求使用更大的缓冲区大小以减少开销。
这个答案有更多信息:
首先,您并不是真正测量磁盘写入速度,而是(部分地)将数据写入 OS 磁盘缓存的速度。要真正衡量磁盘写入速度,应该在计算时间之前将数据刷新到磁盘。如果不刷新,可能会有所不同,具体取决于文件大小和可用内存。
计算好像也有问题。您没有使用 MB
.
的值
还要确保缓冲区大小是 2 的幂,或者至少是磁盘页面大小(4096 字节)的倍数:char buffer[32 * 1024];
。您也可以为 payload
这样做。 (看起来您在添加计算的编辑中将其从 1024 更改为 1000)。
不要使用流将(二进制)数据缓冲区写入磁盘,而是使用 FILE*, fopen(), fwrite(), fclose()
直接写入文件。有关示例和一些时间安排,请参阅 this answer。
要复制文件:以只读模式打开源文件,如果可能,以只进模式打开,并使用 fread(), fwrite()
:
while fread() from source to buffer
fwrite() buffer to destination file
这应该为您提供与 OS 文件复制速度相当的速度(您可能想测试一些不同的缓冲区大小)。
使用内存映射可能稍微快一些:
open src, create memory mapping over the file
open/create dest, set file size to size of src, create memory mapping over the file
memcpy() src to dest
对于大文件,应使用较小的映射视图。
问题是您为 fstream 指定的缓冲区太小
char buffer[32 * 1000];
std::ofstream of("test.file");
of.rdbuf()->pubsetbuf(buffer, sizeof(buffer));
您的应用在用户模式下运行。要写入磁盘,ofstream 调用系统 write function that executed in kernel mode. Then write 将数据传输到系统缓存,然后传输到 HDD 缓存,然后将其写入磁盘。
此缓冲区大小会影响系统调用的次数(每 32*1000 字节调用 1 次)。在系统调用期间 OS 必须将执行上下文从用户模式切换到内核模式然后再切换回来。切换上下文是开销。在 Linux 中,它相当于大约 2500-3500 个简单的 CPU 命令。因此,您的应用在上下文切换上花费的时间最多 CPU。
在您的第二个应用中,您使用
FILE* file = fopen("test.file", "w");
FILE 默认使用更大的缓冲区,这就是它产生更高效代码的原因。您可以尝试使用 setvbuf 指定小缓冲区。在这种情况下,您应该会看到相同的性能下降。
请注意,在您的情况下,瓶颈不是硬盘性能。是上下文切换
使用下面的程序,我尝试测试使用 std::ofstream
写入磁盘的速度。
我在写一个 1 GiB 的文件时达到了大约 300 MiB/s。
但是,使用 cp
命令进行简单的文件复制至少要快两倍。
我的程序是否达到了硬件限制或者是否可以提高速度?
#include <chrono>
#include <iostream>
#include <fstream>
char payload[1000 * 1000]; // 1 MB
void test(int MB)
{
// Configure buffer
char buffer[32 * 1000];
std::ofstream of("test.file");
of.rdbuf()->pubsetbuf(buffer, sizeof(buffer));
auto start_time = std::chrono::steady_clock::now();
// Write a total of 1 GB
for (auto i = 0; i != MB; ++i)
{
of.write(payload, sizeof(payload));
}
double elapsed_ns = std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::steady_clock::now() - start_time).count();
double megabytes_per_ns = 1e3 / elapsed_ns;
double megabytes_per_s = 1e9 * megabytes_per_ns;
std::cout << "Payload=" << MB << "MB Speed=" << megabytes_per_s << "MB/s" << std::endl;
}
int main()
{
for (auto i = 1; i <= 10; ++i)
{
test(i * 100);
}
}
输出:
Payload=100MB Speed=3792.06MB/s
Payload=200MB Speed=1790.41MB/s
Payload=300MB Speed=1204.66MB/s
Payload=400MB Speed=910.37MB/s
Payload=500MB Speed=722.704MB/s
Payload=600MB Speed=579.914MB/s
Payload=700MB Speed=499.281MB/s
Payload=800MB Speed=462.131MB/s
Payload=900MB Speed=411.414MB/s
Payload=1000MB Speed=364.613MB/s
更新
我从std::ofstream
变成了fwrite
:
#include <chrono>
#include <cstdio>
#include <iostream>
char payload[1024 * 1024]; // 1 MiB
void test(int number_of_megabytes)
{
FILE* file = fopen("test.file", "w");
auto start_time = std::chrono::steady_clock::now();
// Write a total of 1 GB
for (auto i = 0; i != number_of_megabytes; ++i)
{
fwrite(payload, 1, sizeof(payload), file );
}
fclose(file); // TODO: RAII
double elapsed_ns = std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::steady_clock::now() - start_time).count();
double megabytes_per_ns = 1e3 / elapsed_ns;
double megabytes_per_s = 1e9 * megabytes_per_ns;
std::cout << "Size=" << number_of_megabytes << "MiB Duration=" << long(0.5 + 100 * elapsed_ns/1e9)/100.0 << "s Speed=" << megabytes_per_s << "MiB/s" << std::endl;
}
int main()
{
test(256);
test(512);
test(1024);
test(1024);
}
将 1 GiB 文件的速度提高到 668MiB/s:
Size=256MiB Duration=0.4s Speed=2524.66MiB/s
Size=512MiB Duration=0.79s Speed=1262.41MiB/s
Size=1024MiB Duration=1.5s Speed=664.521MiB/s
Size=1024MiB Duration=1.5s Speed=668.85MiB/s
与 dd
:
time dd if=/dev/zero of=test.file bs=1024 count=0 seek=1048576
real 0m1.539s
user 0m0.001s
sys 0m0.344s
- Streams are slow
cp
直接使用系统调用read(2)
或mmap(2)
.
我敢打赌,它是 CP 或文件系统内部的一些聪明东西。如果它在 CP 中,则可能是您正在复制的文件中有很多 0,而 cp 正在检测到这一点并写入文件的 sparse 版本。 cp 的手册页说 "By default, sparse SOURCE files are detected by a crude heuristic and the corresponding DEST file is made sparse as well." 这可能意味着一些事情,但其中之一是 cp 可以制作文件的稀疏版本,这将需要更少的磁盘写入时间。
如果它在您的文件系统中,那么它可能是 Deduplication。
作为一个远景第三,它也可能是你的 OS 或你的磁盘固件中的某些东西,它正在将读写转换成一些不需要像你的程序那样同步的专用指令需要(较低的总线使用意味着较少的延迟)。
您使用的缓冲区大小相对较小。小缓冲区意味着每秒有更多的操作,这会增加开销。磁盘系统在收到 read/write 请求并开始处理之前会有少量延迟;更大的缓冲区摊销成本更好一些。较小的缓冲区也可能意味着磁盘花费更多的时间寻找。
您不会同时发出多个请求 - 您需要先完成一次读取,然后才能开始下一次读取。这意味着磁盘可能有死区时间,它什么都不做。由于所有写入都依赖于所有读取,并且您的读取是串行的,因此您正在使磁盘系统缺少读取请求(双重情况,因为写入会占用读取)。
所有读取请求的请求读取字节总数应大于磁盘系统的带宽延迟乘积。如果磁盘有 0.5 毫秒的延迟和 4 GB/sec 的性能,那么您希望始终有 4 GB * 0.5 毫秒 = 2 MB 的未完成读取。
您没有使用任何操作系统提示您正在执行顺序读取。
解决这个问题:
- 更改您的代码以始终有多个未完成的读取请求。
- 有足够的未完成读取请求,因此您正在等待至少 2 MB 的数据。
- 使用 posix_fadvise() 标志帮助 OS 磁盘调度和页面缓存优化。
- 考虑使用 mmap 来减少开销。
- 每个读取请求使用更大的缓冲区大小以减少开销。
这个答案有更多信息:
首先,您并不是真正测量磁盘写入速度,而是(部分地)将数据写入 OS 磁盘缓存的速度。要真正衡量磁盘写入速度,应该在计算时间之前将数据刷新到磁盘。如果不刷新,可能会有所不同,具体取决于文件大小和可用内存。
计算好像也有问题。您没有使用 MB
.
还要确保缓冲区大小是 2 的幂,或者至少是磁盘页面大小(4096 字节)的倍数:char buffer[32 * 1024];
。您也可以为 payload
这样做。 (看起来您在添加计算的编辑中将其从 1024 更改为 1000)。
不要使用流将(二进制)数据缓冲区写入磁盘,而是使用 FILE*, fopen(), fwrite(), fclose()
直接写入文件。有关示例和一些时间安排,请参阅 this answer。
要复制文件:以只读模式打开源文件,如果可能,以只进模式打开,并使用 fread(), fwrite()
:
while fread() from source to buffer
fwrite() buffer to destination file
这应该为您提供与 OS 文件复制速度相当的速度(您可能想测试一些不同的缓冲区大小)。
使用内存映射可能稍微快一些:
open src, create memory mapping over the file
open/create dest, set file size to size of src, create memory mapping over the file
memcpy() src to dest
对于大文件,应使用较小的映射视图。
问题是您为 fstream 指定的缓冲区太小
char buffer[32 * 1000];
std::ofstream of("test.file");
of.rdbuf()->pubsetbuf(buffer, sizeof(buffer));
您的应用在用户模式下运行。要写入磁盘,ofstream 调用系统 write function that executed in kernel mode. Then write 将数据传输到系统缓存,然后传输到 HDD 缓存,然后将其写入磁盘。
此缓冲区大小会影响系统调用的次数(每 32*1000 字节调用 1 次)。在系统调用期间 OS 必须将执行上下文从用户模式切换到内核模式然后再切换回来。切换上下文是开销。在 Linux 中,它相当于大约 2500-3500 个简单的 CPU 命令。因此,您的应用在上下文切换上花费的时间最多 CPU。
在您的第二个应用中,您使用
FILE* file = fopen("test.file", "w");
FILE 默认使用更大的缓冲区,这就是它产生更高效代码的原因。您可以尝试使用 setvbuf 指定小缓冲区。在这种情况下,您应该会看到相同的性能下降。
请注意,在您的情况下,瓶颈不是硬盘性能。是上下文切换