Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FileSystem.Unix: improve CopyFile. #59695

Merged
merged 9 commits into from
Nov 15, 2021
Merged

Conversation

tmds
Copy link
Member

@tmds tmds commented Sep 28, 2021

Like the upcoming version of GNU coreutils 'cp' prefer a copy-on-write clone.
This shares the physical storage between files, which means no data needs to copied.
CoW-clones are supported by a number of Linux file systems, like Btrfs, XFS, and overlayfs.

Eliminate a 'stat' call that is always performed for checking if the target is a directory
by only performing the check when the 'open' syscall reports an error.

Eliminate a 'stat' call for retrieving the file size of the source by passing through
the length that was retrieved when checking the opened file is not a directory.

Create the destination with file permissions that match the source.
We still need to fchmod due to umask being applied to the open mode.

When performing a manual copy, limit the allocated buffer for small files.
And, avoid the last 'read' call by checking when we've copied the expected nr of bytes.

@ghost ghost added the community-contribution Indicates that the PR has been added by a community member label Sep 28, 2021
@ghost
Copy link

ghost commented Sep 28, 2021

Tagging subscribers to this area: @dotnet/area-system-io
See info in area-owners.md if you want to be subscribed.

Issue Details

Like the upcoming version of GNU coreutils 'cp' prefer a copy-on-write clone.
This shares the physical storage between files, which means no data needs to copied.
CoW-clones are supported by a number of Linux file systems, like Btrfs, XFS, and overlayfs.

Eliminate a 'stat' call that is always performed for checking if the target is a directory
by only performing the check when the 'open' syscall reports an error.

Eliminate a 'stat' call for retrieving the file size of the source by passing through
the length that was retrieved when checking the opened file is not a directory.

Create the destination with file permissions that match the source.
We still need to fchmod due to umask being applied to the open mode.

When performing a manual copy, limit the allocated buffer for small files.
And, avoid the last 'read' call by checking when we've copied the expected nr of bytes.

Author: tmds
Assignees: -
Labels:

area-System.IO, community-contribution

Milestone: -

@adamsitnik
Copy link
Member

Hi @tmds

Could you please provide some benchmark results for small, medium and big files? The change is non-trivial, it would be good to make sure it's worth the complexity.

Thanks!

@tmds
Copy link
Member Author

tmds commented Oct 19, 2021

For small files, we see an improvement due to eliminating syscalls.

For large files, this doesn't weigh in, and we see a huge gain on the filesystem that support CoW (my home partition uses Btrfs which supports it, while /tmp doesn't support it).

Method Job Toolchain SourceSize BaseDir Mean Error StdDev Median Ratio RatioSD
FileCopy Job-ABJWHG /orig/corerun 0 /home/tmds 5.815 ms 0.1136 ms 0.1700 ms 5.830 ms 1.00 0.00
FileCopy Job-EQFSPF /pr/corerun 0 /home/tmds 5.731 ms 0.1138 ms 0.0888 ms 5.714 ms 0.97 0.04
FileCopy Job-ABJWHG /orig/corerun 0 /tmp/ 1.470 ms 0.0065 ms 0.0061 ms 1.469 ms 1.00 0.00
FileCopy Job-EQFSPF /pr/corerun 0 /tmp/ 1.274 ms 0.0086 ms 0.0076 ms 1.273 ms 0.87 0.01
FileCopy Job-ABJWHG /orig/corerun 1 /home/tmds 7.611 ms 0.1449 ms 0.2213 ms 7.623 ms 1.00 0.00
FileCopy Job-EQFSPF /pr/corerun 1 /home/tmds 6.828 ms 0.1350 ms 0.1607 ms 6.847 ms 0.89 0.03
FileCopy Job-ABJWHG /orig/corerun 1 /tmp/ 1.970 ms 0.0215 ms 0.0191 ms 1.967 ms 1.00 0.00
FileCopy Job-EQFSPF /pr/corerun 1 /tmp/ 1.700 ms 0.0085 ms 0.0075 ms 1.700 ms 0.86 0.01
FileCopy Job-ABJWHG /orig/corerun 100 /home/tmds 7.442 ms 0.1417 ms 0.1740 ms 7.424 ms 1.00 0.00
FileCopy Job-EQFSPF /pr/corerun 100 /home/tmds 6.507 ms 0.1284 ms 0.2036 ms 6.375 ms 0.88 0.04
FileCopy Job-ABJWHG /orig/corerun 100 /tmp/ 1.945 ms 0.0285 ms 0.0266 ms 1.951 ms 1.00 0.00
FileCopy Job-EQFSPF /pr/corerun 100 /tmp/ 1.717 ms 0.0341 ms 0.0560 ms 1.698 ms 0.91 0.04
FileCopy Job-ABJWHG /orig/corerun 512 /home/tmds 7.458 ms 0.1465 ms 0.1505 ms 7.478 ms 1.00 0.00
FileCopy Job-EQFSPF /pr/corerun 512 /home/tmds 6.480 ms 0.1269 ms 0.1737 ms 6.448 ms 0.87 0.03
FileCopy Job-ABJWHG /orig/corerun 512 /tmp/ 1.892 ms 0.0107 ms 0.0100 ms 1.893 ms 1.00 0.00
FileCopy Job-EQFSPF /pr/corerun 512 /tmp/ 1.697 ms 0.0086 ms 0.0067 ms 1.694 ms 0.90 0.01
FileCopy Job-ABJWHG /orig/corerun 1024 /home/tmds 7.435 ms 0.1481 ms 0.2670 ms 7.448 ms 1.00 0.00
FileCopy Job-EQFSPF /pr/corerun 1024 /home/tmds 6.426 ms 0.1271 ms 0.1608 ms 6.435 ms 0.87 0.04
FileCopy Job-ABJWHG /orig/corerun 1024 /tmp/ 1.988 ms 0.0136 ms 0.0121 ms 1.989 ms 1.00 0.00
FileCopy Job-EQFSPF /pr/corerun 1024 /tmp/ 1.709 ms 0.0138 ms 0.0122 ms 1.710 ms 0.86 0.01
FileCopy Job-ABJWHG /orig/corerun 4096 /home/tmds 7.241 ms 0.1404 ms 0.1725 ms 7.182 ms 1.00 0.00
FileCopy Job-EQFSPF /pr/corerun 4096 /home/tmds 6.469 ms 0.1280 ms 0.2138 ms 6.414 ms 0.90 0.03
FileCopy Job-ABJWHG /orig/corerun 4096 /tmp/ 1.940 ms 0.0190 ms 0.0178 ms 1.944 ms 1.00 0.00
FileCopy Job-EQFSPF /pr/corerun 4096 /tmp/ 1.679 ms 0.0063 ms 0.0052 ms 1.679 ms 0.86 0.01
FileCopy Job-ABJWHG /orig/corerun 1048576 /home/tmds 42.591 ms 0.3892 ms 0.3250 ms 42.655 ms 1.00 0.00
FileCopy Job-EQFSPF /pr/corerun 1048576 /home/tmds 6.524 ms 0.1250 ms 0.1488 ms 6.540 ms 0.15 0.00
FileCopy Job-ABJWHG /orig/corerun 1048576 /tmp/ 30.270 ms 0.2621 ms 0.2451 ms 30.248 ms 1.00 0.00
FileCopy Job-EQFSPF /pr/corerun 1048576 /tmp/ 30.401 ms 0.5868 ms 0.8602 ms 30.213 ms 1.02 0.03
FileCopy Job-ABJWHG /orig/corerun 10485760 /home/tmds 401.622 ms 2.6749 ms 2.2337 ms 401.577 ms 1.00 0.00
FileCopy Job-EQFSPF /pr/corerun 10485760 /home/tmds 6.391 ms 0.1265 ms 0.2344 ms 6.326 ms 0.02 0.00
FileCopy Job-ABJWHG /orig/corerun 10485760 /tmp/ 368.813 ms 6.8000 ms 6.6785 ms 368.475 ms 1.00 0.00
FileCopy Job-EQFSPF /pr/corerun 10485760 /tmp/ 369.906 ms 5.2226 ms 4.8852 ms 370.370 ms 1.00 0.02

Benchmark:

using System;
using System.Collections.Generic;
using System.IO;
using BenchmarkDotNet;
using BenchmarkDotNet.Attributes;

namespace FileCopyBenchmark
{
    public class Benchmarks
    {
        private const int kB  = 1 << 10;
        private const int MB = kB  << 10;

        [Params(0, 1, 100, 512, 1 * kB,  4 * kB, 1 * MB, 10 * MB)]
        public int SourceSize;

        [ParamsSource(nameof(ValuesForBaseDir))]
        public string BaseDir { get; set; }

        public IEnumerable<string> ValuesForBaseDir =>
        new[] { Path.GetTempPath(), Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments) };

        private string SourceFileName;
        private string DestinationFileName;

        [GlobalSetup]
        public void GlobalSetup()
        {
            DestinationFileName = Path.Combine(BaseDir, Guid.NewGuid().ToString());
            SourceFileName = Path.Combine(BaseDir, Guid.NewGuid().ToString());
            using FileStream random = File.OpenRead("/dev/random");
            using FileStream sourceFile = File.OpenWrite(SourceFileName);
            Span<byte> buffer = stackalloc byte[4 * kB];
            int length = SourceSize;
            while (length > 0)
            {
                int read = random.Read(buffer.Slice(0, Math.Min(length, buffer.Length)));
                sourceFile.Write(buffer.Slice(0, read));
                length -= read;
            }
        }

        [GlobalCleanup]
        public void GlobalCleanup()
        {
            File.Delete(SourceFileName);
            File.Delete(DestinationFileName);
        }

        [Benchmark]
        public void FileCopy()
        {
            for (int i = 0; i < 100; i++)
            {
                File.Delete(DestinationFileName);
                File.Copy(SourceFileName, DestinationFileName);
            }
        }
    }
}

@stephentoub
Copy link
Member

we see a huge gain on the filesystem that support CoW

What is the impact on subsequent writes to the file?

@tmds
Copy link
Member Author

tmds commented Oct 19, 2021

What is the impact on subsequent writes to the file?

The copies get avoided by the CoW, but then when you write to the file, you are making those copies again.
The copies are made at the block level, so only for the parts of the file that are written to.

Updating the benchmark to overwrite half of the file after File.Copy shows the 50% ratio in the benchmark results.

        [Benchmark]
        public void FileCopy()
        {
            Span<byte> buffer = stackalloc byte[4 * kB];
            for (int i = 0; i < 100; i++)
            {
                File.Delete(DestinationFileName);
                File.Copy(SourceFileName, DestinationFileName);
                using SafeFileHandle writeHandle = File.OpenHandle(DestinationFileName, FileMode.Open, FileAccess.Write, FileShare.Write);
                for (int offset = SourceSize / 2; offset < SourceSize; )
                {
                    RandomAccess.Write(writeHandle, buffer, offset);
                    offset += buffer.Length;
                }
            }
        }
Method Job Toolchain SourceSize BaseDir Mean Error StdDev Ratio
FileCopy Job-XVSSYH /orig/corerun 10485760 /home/tmds 974.1 ms 6.15 ms 5.14 ms 1.00
FileCopy Job-QDDOUC /pr/corerun 10485760 /home/tmds 484.8 ms 3.47 ms 3.25 ms 0.50

Like the upcoming version of GNU coreutils 'cp' prefer a copy-on-write clone.
This shares the physical storage between files, which means no data needs to copied.
CoW-clones are supported by a number of Linux file systems, like Btrfs, XFS, and overlayfs.

Eliminate a 'stat' call that is always performed for checking if the target is a directory
by only performing the check when the 'open' syscall reports an error.

Eliminate a 'stat' call for retrieving the file size of the source by passing through
the length that was retrieved when checking the opened file is not a directory.

Create the destination with file permissions that match the source.
We still need to fchmod due to umask being applied to the open mode.

When performing a manual copy, limit the allocated buffer for small files.
And, avoid the last 'read' call by checking when we've copied the expected nr of bytes.
@tmds
Copy link
Member Author

tmds commented Oct 19, 2021

I had messed up the PR by resolving conflicts in the GitHub UI.
I've rebased on my machine.

@tmds
Copy link
Member Author

tmds commented Oct 20, 2021

I ran the benchmark once more to ensure there are no regressions. I've included some more sizes.

Method Job Toolchain SourceSize BaseDir Mean Error StdDev Ratio RatioSD
FileCopy Job-JGOCUT /orig/corerun 0 /home/tmds 3.744 ms 0.0120 ms 0.0100 ms 1.00 0.00
FileCopy Job-WWULDC /pr/corerun 0 /home/tmds 3.479 ms 0.0244 ms 0.0217 ms 0.93 0.01
FileCopy Job-JGOCUT /orig/corerun 0 /tmp/ 1.358 ms 0.0059 ms 0.0053 ms 1.00 0.00
FileCopy Job-WWULDC /pr/corerun 0 /tmp/ 1.150 ms 0.0048 ms 0.0042 ms 0.85 0.00
FileCopy Job-JGOCUT /orig/corerun 1 /home/tmds 4.770 ms 0.0240 ms 0.0224 ms 1.00 0.00
FileCopy Job-WWULDC /pr/corerun 1 /home/tmds 4.337 ms 0.0149 ms 0.0140 ms 0.91 0.01
FileCopy Job-JGOCUT /orig/corerun 1 /tmp/ 1.762 ms 0.0055 ms 0.0049 ms 1.00 0.00
FileCopy Job-WWULDC /pr/corerun 1 /tmp/ 1.584 ms 0.0091 ms 0.0085 ms 0.90 0.01
FileCopy Job-JGOCUT /orig/corerun 100 /home/tmds 4.753 ms 0.0342 ms 0.0286 ms 1.00 0.00
FileCopy Job-WWULDC /pr/corerun 100 /home/tmds 4.334 ms 0.0239 ms 0.0212 ms 0.91 0.00
FileCopy Job-JGOCUT /orig/corerun 100 /tmp/ 1.746 ms 0.0060 ms 0.0053 ms 1.00 0.00
FileCopy Job-WWULDC /pr/corerun 100 /tmp/ 1.561 ms 0.0079 ms 0.0070 ms 0.89 0.00
FileCopy Job-JGOCUT /orig/corerun 512 /home/tmds 4.835 ms 0.0168 ms 0.0157 ms 1.00 0.00
FileCopy Job-WWULDC /pr/corerun 512 /home/tmds 4.365 ms 0.0277 ms 0.0259 ms 0.90 0.00
FileCopy Job-JGOCUT /orig/corerun 512 /tmp/ 1.764 ms 0.0105 ms 0.0093 ms 1.00 0.00
FileCopy Job-WWULDC /pr/corerun 512 /tmp/ 1.530 ms 0.0072 ms 0.0067 ms 0.87 0.01
FileCopy Job-JGOCUT /orig/corerun 1024 /home/tmds 4.742 ms 0.0266 ms 0.0236 ms 1.00 0.00
FileCopy Job-WWULDC /pr/corerun 1024 /home/tmds 4.284 ms 0.0160 ms 0.0150 ms 0.90 0.00
FileCopy Job-JGOCUT /orig/corerun 1024 /tmp/ 1.765 ms 0.0033 ms 0.0027 ms 1.00 0.00
FileCopy Job-WWULDC /pr/corerun 1024 /tmp/ 1.570 ms 0.0060 ms 0.0053 ms 0.89 0.00
FileCopy Job-JGOCUT /orig/corerun 4096 /home/tmds 4.722 ms 0.0144 ms 0.0135 ms 1.00 0.00
FileCopy Job-WWULDC /pr/corerun 4096 /home/tmds 4.467 ms 0.0349 ms 0.0327 ms 0.95 0.01
FileCopy Job-JGOCUT /orig/corerun 4096 /tmp/ 1.771 ms 0.0061 ms 0.0054 ms 1.00 0.00
FileCopy Job-WWULDC /pr/corerun 4096 /tmp/ 1.566 ms 0.0043 ms 0.0040 ms 0.88 0.00
FileCopy Job-JGOCUT /orig/corerun 8192 /home/tmds 4.884 ms 0.0133 ms 0.0118 ms 1.00 0.00
FileCopy Job-WWULDC /pr/corerun 8192 /home/tmds 4.297 ms 0.0137 ms 0.0128 ms 0.88 0.00
FileCopy Job-JGOCUT /orig/corerun 8192 /tmp/ 2.002 ms 0.0168 ms 0.0158 ms 1.00 0.00
FileCopy Job-WWULDC /pr/corerun 8192 /tmp/ 1.814 ms 0.0029 ms 0.0022 ms 0.91 0.01
FileCopy Job-JGOCUT /orig/corerun 16384 /home/tmds 5.110 ms 0.0267 ms 0.0237 ms 1.00 0.00
FileCopy Job-WWULDC /pr/corerun 16384 /home/tmds 4.407 ms 0.0148 ms 0.0131 ms 0.86 0.01
FileCopy Job-JGOCUT /orig/corerun 16384 /tmp/ 2.186 ms 0.0072 ms 0.0067 ms 1.00 0.00
FileCopy Job-WWULDC /pr/corerun 16384 /tmp/ 2.013 ms 0.0064 ms 0.0056 ms 0.92 0.00
FileCopy Job-JGOCUT /orig/corerun 32768 /home/tmds 5.584 ms 0.0166 ms 0.0147 ms 1.00 0.00
FileCopy Job-WWULDC /pr/corerun 32768 /home/tmds 4.297 ms 0.0162 ms 0.0152 ms 0.77 0.00
FileCopy Job-JGOCUT /orig/corerun 32768 /tmp/ 2.653 ms 0.0074 ms 0.0069 ms 1.00 0.00
FileCopy Job-WWULDC /pr/corerun 32768 /tmp/ 2.477 ms 0.0081 ms 0.0072 ms 0.93 0.00
FileCopy Job-JGOCUT /orig/corerun 1048576 /home/tmds 33.937 ms 0.5533 ms 0.5175 ms 1.00 0.00
FileCopy Job-WWULDC /pr/corerun 1048576 /home/tmds 3.809 ms 0.0079 ms 0.0066 ms 0.11 0.00
FileCopy Job-JGOCUT /orig/corerun 1048576 /tmp/ 29.520 ms 0.3540 ms 0.3311 ms 1.00 0.00
FileCopy Job-WWULDC /pr/corerun 1048576 /tmp/ 29.888 ms 0.4000 ms 0.3742 ms 1.01 0.01
FileCopy Job-JGOCUT /orig/corerun 10485760 /home/tmds 418.623 ms 5.8803 ms 5.5004 ms 1.000 0.00
FileCopy Job-WWULDC /pr/corerun 10485760 /home/tmds 3.742 ms 0.0050 ms 0.0047 ms 0.009 0.00
FileCopy Job-JGOCUT /orig/corerun 10485760 /tmp/ 404.851 ms 4.3733 ms 4.0908 ms 1.00 0.00
FileCopy Job-WWULDC /pr/corerun 10485760 /tmp/ 401.042 ms 5.0462 ms 4.7202 ms 0.99 0.02

@tmds
Copy link
Member Author

tmds commented Oct 28, 2021

@stephentoub all feedback is addressed and CI is happy. This is good to merge.

Copy link
Contributor

@deeprobin deeprobin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few small suggestions about coding style, but nothing important.
LGTM

@tmds
Copy link
Member Author

tmds commented Nov 4, 2021

@stephentoub @dotnet/area-system-io can you merge this?

@jeffhandley jeffhandley merged commit 9b83294 into dotnet:main Nov 15, 2021
@jeffhandley
Copy link
Member

Thanks as always, @tmds!

Interop.Sys.Permissions filePermissions;
using SafeFileHandle src = SafeFileHandle.OpenReadOnly(sourceFullPath, FileOptions.None, out fileLength, out filePermissions);
using SafeFileHandle dst = SafeFileHandle.Open(destFullPath, overwrite ? FileMode.Create : FileMode.CreateNew,
FileAccess.ReadWrite, FileShare.None, FileOptions.None, preallocationSize: 0, openPermissions: filePermissions,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder whether it would be beneficial to provide preallocationSize: fileLength here. @tmds what do you think?

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
area-System.IO community-contribution Indicates that the PR has been added by a community member
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants