Skip to content

Clone files on OSX-like platforms when possible, instead of copying the whole file - #79243

Merged
adamsitnik merged 85 commits into
dotnet:mainfrom
hamarb123:main
Jun 13, 2023
Merged

Clone files on OSX-like platforms when possible, instead of copying the whole file#79243
adamsitnik merged 85 commits into
dotnet:mainfrom
hamarb123:main

Conversation

@hamarb123

@hamarb123hamarb123 commented Dec 5, 2022

Copy link
Copy Markdown
Contributor

Fixes#77835.

This should improve performance greatly for File.Copy when copying from one APFS volume to a different file in the volume (I think it's within a volume anyway). It will clone the file which is instant instead of copying all of the bytes, and if cloning doesn't work it will fall back to the normal method of copying.

Informative:

  • Uses clonefile when possible on macOS to clone the file while still keeping file locking logic intact
  • Split common Unix logic into multiple functions that the macOS implementation uses parts of at different times
  • No shim needed since the ABI will not change. Precedent for doing it this way for macOS-specific functions: Fix setting creation date for OSX-like platforms #49555
  • Applies to all OSX-like platforms (macOS, iOS, tvOS, and Mac Catalyst), not just macOS

Todo:

  • Need to add tests to check the file is actually cloned so we know if it works or not
  • The logic in this commit has not been tested thoroughly (but it has been tested somewhat), so will wait to see the GitHub actions failures before refactoring (if needed)
  • Do we want tests copying between different "drives"? I think these would be valuable.

• Use copyfile (with COPYFILE_CLONE_FORCE) when possible on macOS to clone the file while still keeping file locking logic intact
• Split common Unix logic into multiple functions that the macOS implementation uses parts of at different times
• Add string version of ResolveLinkTarget to save the allocation since part of the code needs it
• Need to add tests to check the file is actually cloned so we know if it works or not
@ghostghost added area-System.IO community-contribution Indicates that the PR has been added by a community member labels Dec 5, 2022
@ghost

ghost commented Dec 5, 2022

Copy link
Copy Markdown

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

Issue Details

Fixes #77835.

This should improve performance greatly for File.Copy when copying from one APFS volume to a different file in the volume (I think it's within a volume anyway). It will clone the file which is instant instead of copying all of the bytes, and if cloning doesn't work it will fall back to the normal method of copying.

Informative:

  • Uses copyfile (with COPYFILE_CLONE_FORCE) when possible on macOS to clone the file while still keeping file locking logic intact
  • Split common Unix logic into multiple functions that the macOS implementation uses parts of at different times
  • No shim needed since the ABI will not change. Precedent for doing it this way for macOS-specific functions: Fix setting creation date for OSX-like platforms #49555
  • Applies to all OSX-like platforms, not just macOS

Todo:

  • Need to add tests to check the file is actually cloned so we know if it works or not
  • The logic in this commit has not been tested thoroughly (but it has been tested somewhat), so will wait to see the GitHub actions issues before refactoring (if needed)
  • Is it a problem that it could theoretically delete the destination file and then fail for some other reason, leaving the destination file deleted when it may not have done so before (realistically this is extremely unlikely)?
Author:hamarb123
Assignees:-
Labels:

area-System.IO

Milestone:-

@hamarb123

Copy link
Copy Markdown
ContributorAuthor

Btw, this implementation is not done, I want to see if any tests fail first though.

• I missed setting StringMarshalling on the LibraryImport
• I missed partial on the CopyFile method implementations
• Apparently 'Both partial method declarations must be unsafe or neither may be unsafe'
• I love partial methods
• Move unsafe keyword into the OSX method rather than in method declaration
• Fix accessibility modifier of CopyFile in FileSystem.Unix.cs
• Fix indentation (should have used spaces)
• Import Microsoft.Win32.SafeHandles in FileSystem.CopyFile.OSX.cs file
• Fix for SA1205 'Partial elements should declare an access modifier'
• Fix typo in FileSystem.CopyFile.OtherUnix.cs of 'startedCopyFile'
• Fix missing openDst parameter code in StartCopyFile
• Fix not checking the nullability in StandardCopyFile, which led to 'Possible null reference argument for parameter'
• Add missing parameter value (openNewFile) to OpenCopyFileDstHandle
• Fix misnamed variable usages throughout some code
• Properly qualify Interop.ErrorInfo
• This should be the last fix for build issues for this set
@hamarb123

Copy link
Copy Markdown
ContributorAuthor

Sorry for all of the fix commits, I couldn't get build locally to work, so I copied the code into a new project to test it and must have missed some of the changes here.

@hamarb123
hamarb123 marked this pull request as draft December 5, 2022 21:44
• Add missing nullable ? for dstHandle (which obtains the file lock)
• Test failures were since copying a file onto itself should cause an error, this fixes that
…ain)
• The variable in the if statements weren't updated
• Handling for if readlink fails
…t#77835'
• The source path should have used sourceFullPath rather than destPath
@marek-safarmarek-safar added the os-mac-os-x macOS aka OSX label Dec 6, 2022
@hamarb123

hamarb123 commented Dec 6, 2022

Copy link
Copy Markdown
ContributorAuthor

@marek-safar, this applies to macOS, iOS, tvOS, and Mac Catalyst in case you wanted to add tags for those. Can you also 'assign' me if you're able? Thanks.

@hamarb123hamarb123 changed the title [WIP] Cloning files on macOS when possible instead of copying the whole file[WIP] Cloning files on OSX-like platforms when possible instead of copying the whole fileDec 6, 2022
@ghostghost closed this Jan 6, 2023
@ghost

ghost commented Jan 6, 2023

Copy link
Copy Markdown

Draft Pull Request was automatically closed for 30 days of inactivity. Please let us know if you'd like to reopen it.

@adamsitnik

Copy link
Copy Markdown
Member

@filipnavara is there any chance you could review this PR? You are currently the most experience person in this particular area.

@hamarb123

Copy link
Copy Markdown
ContributorAuthor

Is there a reason why 24 test runs have been cancelled so far? Do the tests need to be re-run?

@danmoseley

Copy link
Copy Markdown
Contributor

@hamarb123 it is dotnet/arcade#13774

@slang25

Copy link
Copy Markdown
Contributor

I'm really looking forward to this change! Will we be seeing it land in a .NET 8 preview?

@danmoseley

Copy link
Copy Markdown
Contributor

I'm really looking forward to this change! Will we be seeing it land in a .NET 8 preview?

If it's merged in the next month or two, it will be in .NET 8.

@hamarb123

Copy link
Copy Markdown
ContributorAuthor

I'm really looking forward to this change! Will we be seeing it land in a .NET 8 preview?

It's hopefully just a few more stages of review at most. I'm hoping it will make it to .NET 8 also.

@tmds

tmds commented Jun 13, 2023

Copy link
Copy Markdown
Member

Per @adamsitnik's earlier comment, macOS isn't benchmarked in the perf lab.

So before merging, we need some benchmark results that verify there is a performance gain.

@hamarb123 can you look at the documentation Adam shared, and run the benchmarks on your machine?

@hamarb123

hamarb123 commented Jun 13, 2023

Copy link
Copy Markdown
ContributorAuthor

Benchmark results:

  • CopyTo is ~1.9x faster for small files with the changes, and is usually ~180us for all the tests up to 1MB (~207us for 100MB test). Note this test also deletes the file as a part of the test, so simply copying would likely be faster again.
  • CopyToOverwrite is ~1.15x faster for small files with the changes, and is ~215us for all the tests.
  • Copying tests in System.IO.Tests.Perf_FileStream did not improve (haven't included them here), do we want to work out why and improve that API also?
  • The comparison was between the commit just before this comment which merges all of the new runtime changes, and the runtime repo with those same runtime changes but without this PR.

System.IO.Tests.Perf_File

With changes
MethodsizeMeanErrorStdDevMedianMinMaxAllocated
CopyTo512180.4 us3.42 us3.66 us180.0 us173.9 us186.3 us65 B
CopyToOverwrite512217.4 us4.17 us4.10 us217.2 us210.2 us225.9 us129 B
CopyTo4096175.9 us3.41 us3.19 us175.0 us171.2 us182.7 us65 B
CopyToOverwrite4096214.0 us2.41 us2.13 us213.4 us209.8 us218.2 us129 B
CopyTo1048576175.9 us2.41 us2.01 us175.9 us173.4 us179.8 us65 B
CopyToOverwrite1048576212.3 us3.97 us3.31 us212.0 us208.1 us218.1 us129 B
CopyTo104857600206.9 us2.32 us1.94 us206.6 us203.7 us210.8 us65 B
CopyToOverwrite104857600213.9 us3.58 us3.17 us214.2 us209.1 us218.0 us129 B
Without changes
MethodsizeMeanErrorStdDevMedianMinMaxAllocated
CopyTo512347.6 us12.46 us14.35 us344.8 us326.4 us374.2 us129 B
CopyToOverwrite512249.2 us5.56 us6.18 us248.7 us241.4 us261.1 us129 B
CopyTo4096347.1 us7.16 us7.36 us345.2 us335.4 us364.5 us129 B
CopyToOverwrite4096244.6 us3.07 us2.72 us243.9 us240.4 us248.4 us129 B
CopyTo10485761,313.5 us68.13 us72.90 us1,322.9 us1,191.9 us1,437.3 us133 B
CopyToOverwrite10485761,773.5 us321.63 us370.39 us1,730.5 us1,184.8 us2,432.6 us136 B
CopyTo10485760055,961.7 us4,954.14 us5,705.19 us53,346.9 us49,530.1 us65,915.7 us-
CopyToOverwrite10485760055,883.4 us5,472.86 us6,302.55 us54,004.8 us46,797.4 us66,152.4 us-

@tmds

tmds commented Jun 13, 2023

Copy link
Copy Markdown
Member

CopyToOverwrite is ~1.15x faster for small files with the changes

That's a pleasant surprise.

My expectation was to see a regression for small files with overwrite since we're making additional system calls and the benefit of cloning is low for small files.

@adamsitnikadamsitnik left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The code looks very good, I found only one thing that could be improved (PTAL at my comment).

The perf win is very impressive. I've benchmarked it on my old macBook:

BenchmarkDotNet=v0.13.2.2052-nightly, OS=macOS Monterey 12.6.5 (21G531) [Darwin 21.6.0]
Intel Core i7-5557U CPU 3.10GHz (Broadwell), 1 CPU, 4 logical and 2 physical cores
.NET SDK=8.0.100-preview.6.23312.5
[Host] : .NET 8.0.0 (8.0.23.30704), X64 RyuJIT AVX2
PR : .NET 8.0.0 (42.42.42.42424), X64 RyuJIT AVX2
main : .NET 8.0.0 (8.0.23.30704), X64 RyuJIT AVX2
MethodJobsizeMeanStdDevRatio
CopyToPR512438.7 μs106.58 μs0.75
CopyTomain512588.0 μs15.62 μs1.00
CopyToOverwritePR512298.8 μs6.18 μs0.79
CopyToOverwritemain512381.4 μs4.55 μs1.00
CopyToPR4096237.0 μs6.29 μs0.39
CopyTomain4096606.9 μs37.95 μs1.00
CopyToOverwritePR4096293.7 μs7.59 μs0.76
CopyToOverwritemain4096388.1 μs3.02 μs1.00
CopyToPR1048576239.9 μs10.95 μs0.09
CopyTomain10485762,567.9 μs107.61 μs1.00
CopyToOverwritePR1048576298.2 μs11.47 μs0.12
CopyToOverwritemain10485762,585.2 μs63.77 μs1.00
CopyToPR104857600247.0 μs8.00 μs0.001
CopyTomain104857600230,797.9 μs42,923.74 μs1.000
CopyToOverwritePR104857600291.4 μs6.45 μs0.001
CopyToOverwritemain104857600256,931.8 μs2,784.23 μs1.000

Great job @hamarb123 !!

@stephentoub

stephentoub commented Jun 13, 2023

Copy link
Copy Markdown
Member

The perf win is very impressive.

While this isn't a common scenario, in the name of completeness, what does the perf look like if, after copying, the benchmark then modifies the destination file in some way?

@adamsitnik

Copy link
Copy Markdown
Member

While this isn't a common scenario, in the name of completeness, what does the perf look like if, after copying, the benchmark then modifies the destination file in some way?

That is a very good question!

I've modified the benchmarks and just added File.WriteAllBytes(dest, File.ReadAllBytes(source)) right after the call to copy:

[Benchmark]
[Arguments(HalfKibibyte)]
[Arguments(FourKibibytes)]
[Arguments(OneMibibyte)]
[Arguments(HundredMibibytes)]
public void CopyTo_AndModify(int size)
{
File.Delete(_testFilePath);
File.Copy(_filesToRead[size], _testFilePath); // overwrite defaults to false
+ File.WriteAllBytes(_testFilePath, File.ReadAllBytes(_filesToRead[size]));
}
[Benchmark]
[Arguments(HalfKibibyte)]
[Arguments(FourKibibytes)]
[Arguments(OneMibibyte)]
[Arguments(HundredMibibytes)]
public void CopyToOverwrite_AndModify(int size)
{
File.Copy(_filesToRead[size], _testFilePath, overwrite: true);
+ File.WriteAllBytes(_testFilePath, File.ReadAllBytes(_filesToRead[size]));
}

We have still a major win for large files and no regression for smaller files

MethodJobsizeMeanStdDevRatio
CopyTo_AndModifyPR512533.1 μs16.83 μs0.71
CopyTo_AndModifymain512742.4 μs6.98 μs1.00
CopyToOverwrite_AndModifyPR512588.8 μs25.28 μs0.98
CopyToOverwrite_AndModifymain512593.9 μs10.35 μs1.00
CopyTo_AndModifyPR4096540.8 μs15.53 μs0.71
CopyTo_AndModifymain4096767.0 μs13.17 μs1.00
CopyToOverwrite_AndModifyPR4096590.7 μs19.49 μs1.00
CopyToOverwrite_AndModifymain4096594.7 μs5.73 μs1.00
CopyTo_AndModifyPR10485762,383.3 μs84.83 μs0.64
CopyTo_AndModifymain10485763,701.9 μs82.21 μs1.00
CopyToOverwrite_AndModifyPR10485762,358.2 μs69.96 μs0.69
CopyToOverwrite_AndModifymain10485763,412.3 μs87.54 μs1.00
CopyTo_AndModifyPR104857600167,426.8 μs18,209.30 μs0.61
CopyTo_AndModifymain104857600276,862.8 μs29,743.99 μs1.00
CopyToOverwrite_AndModifyPR104857600170,189.9 μs26,538.74 μs0.61
CopyToOverwrite_AndModifymain104857600287,618.2 μs48,952.24 μs1.00

@stephentoub

Copy link
Copy Markdown
Member

File.WriteAllBytes

WriteAllBytes opens the file as Create, which means it's going to effectively delete the destination and write a new file rather than modify the one that's there.

@adamsitnik

Copy link
Copy Markdown
Member

WriteAllBytes opens the file as Create, which means it's going to effectively delete the destination and write a new file rather than modify the one that's there.

Sorry, I forgot about that (BTW it just proves that we should really add #84532)

Updated benchmark:

privatebyte[]_oneKb=newbyte[1024];[Benchmark][Arguments(HalfKibibyte)][Arguments(FourKibibytes)][Arguments(OneMibibyte)][Arguments(HundredMibibytes)]publicvoidCopyTo_AndModify(intsize){File.Delete(_testFilePath);File.Copy(_filesToRead[size],_testFilePath);// overwrite defaults to falseusingFileStreamfs=File.OpenWrite(_testFilePath);fs.Write(_oneKb);}[Benchmark][Arguments(HalfKibibyte)][Arguments(FourKibibytes)][Arguments(OneMibibyte)][Arguments(HundredMibibytes)]publicvoidCopyToOverwrite_AndModify(intsize){File.Copy(_filesToRead[size],_testFilePath,overwrite:true);usingFileStreamfs=File.OpenWrite(_testFilePath);fs.Write(_oneKb);}

And the results:

BenchmarkDotNet=v0.13.2.2052-nightly, OS=macOS Monterey 12.6.5 (21G531) [Darwin 21.6.0]
Intel Core i7-5557U CPU 3.10GHz (Broadwell), 1 CPU, 4 logical and 2 physical cores
.NET SDK=8.0.100-preview.6.23312.5
[Host] : .NET 8.0.0 (8.0.23.30704), X64 RyuJIT AVX2
PR : .NET 8.0.0 (42.42.42.42424), X64 RyuJIT AVX2
main : .NET 8.0.0 (8.0.23.30704), X64 RyuJIT AVX2
IterationCount=40
MethodJobsizeMeanStdDevRatio
CopyTo_AndModifyPR512537.7 μs13.18 μs0.82
CopyTo_AndModifymain512661.3 μs9.98 μs1.00
CopyToOverwrite_AndModifyPR512549.1 μs16.54 μs1.15
CopyToOverwrite_AndModifymain512476.3 μs7.58 μs1.00
CopyTo_AndModifyPR4096669.8 μs20.57 μs1.01
CopyTo_AndModifymain4096662.4 μs11.60 μs1.00
CopyToOverwrite_AndModifyPR4096695.3 μs26.41 μs1.50
CopyToOverwrite_AndModifymain4096465.0 μs4.94 μs1.00
CopyTo_AndModifyPR1048576722.5 μs14.36 μs0.35
CopyTo_AndModifymain10485762,051.8 μs40.83 μs1.00
CopyToOverwrite_AndModifyPR1048576728.4 μs34.28 μs0.37
CopyToOverwrite_AndModifymain10485761,991.1 μs163.53 μs1.00
CopyTo_AndModifyPR104857600804.9 μs18.60 μs0.007
CopyTo_AndModifymain104857600109,415.3 μs8,542.98 μs1.000
CopyToOverwrite_AndModifyPR104857600762.1 μs26.40 μs0.007
CopyToOverwrite_AndModifymain104857600117,399.2 μs16,068.51 μs1.000

@adamsitnik
adamsitnik merged commit 8fb25a8 into dotnet:mainJun 13, 2023
@adamsitnikadamsitnik added this to the 8.0.0 milestone Jun 13, 2023
@stephentoub

Copy link
Copy Markdown
Member

BTW it just proves that we should really add #84532

Not really 😄

Updated benchmark

Thanks. So this does regress small overwrites that are subsequently modified.

@adamsitnik

Copy link
Copy Markdown
Member

Not really

Why? Do you believe that appending a buffer of bytes to an existing file is a rare scenario?

@danmoseley

Copy link
Copy Markdown
Contributor

Thanks for this nice change @hamarb123 . Would you like to tackle another issue? There are also a number marked api-approved that are available.

@stephentoub

stephentoub commented Jun 13, 2023

Copy link
Copy Markdown
Member

BTW it just proves that we should really add #84532

Not really

Why? Do you believe that appending a buffer of bytes to an existing file is a rare scenario?

I'm not commenting on the scenario of appending a buffer of bytes to an existing file. I'm commenting that a need to mutate a recently copied file as part of benchmarking a copy-on-write optimization and to achieve that writing two short lines of code doesn't prove that we need to add a new API.

@hamarb123

Copy link
Copy Markdown
ContributorAuthor

Thanks for this nice change @hamarb123 . Would you like to tackle another issue? There are also a number marked api-approved that are available.

Thanks! I'll have a look tomorrow (also known as a today at a sensible hour lol). I was also going to do the one for ReFS on Windows, was setting up a VM to test dev drives yesterday.

Copying tests in System.IO.Tests.Perf_FileStream did not improve (haven't included them here), do we want to work out why and improve that API also?

Just following up on this also. Presumably this API just does a block copy with the files. Is this something that's commonly used (I know I've never done it like this)? Because it doesn't seem to benefit from this, but maybe no-one uses it, idk.

@adamsitnik

Copy link
Copy Markdown
Member

Copying tests in System.IO.Tests.Perf_FileStream did not improve

IIRC The FileStream.CopyTo(Stream destination) has no special handling for cases where the destination stream is also a file stream.

Is this something that's commonly used

I don't have enough data to answer your question, but if you can provide an optimization that improves such scenario without introducing a lot of code complexity I am happy to review it.

@danmoseley

Copy link
Copy Markdown
Contributor

I was also going to do the one for ReFS on Windows

Ah yes, indeed that would be nice to get into .NET 8

@ghostghost locked as resolved and limited conversation to collaborators Jul 15, 2023
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-System.IOcommunity-contributionIndicates that the PR has been added by a community memberos-mac-os-xmacOS aka OSX

Projects

None yet

Development

Successfully merging this pull request may close these issues.

File.Copy: Clone file when possible on MacOS

10 participants

@hamarb123@adamsitnik@filipnavara@akoeplinger@stephentoub@danmoseley@jkotas@tmds@slang25@marek-safar
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Clone files on OSX-like platforms when possible, instead of copying the whole file by hamarb123 · Pull Request #79243 · dotnet/runtime · GitHub
Skip to content

Clone files on OSX-like platforms when possible, instead of copying the whole file - #79243

Merged
adamsitnik merged 85 commits into
dotnet:mainfrom
hamarb123:main
Jun 13, 2023
Merged

Clone files on OSX-like platforms when possible, instead of copying the whole file#79243
adamsitnik merged 85 commits into
dotnet:mainfrom
hamarb123:main

Conversation

@hamarb123

@hamarb123hamarb123 commented Dec 5, 2022

Copy link
Copy Markdown
Contributor

Fixes#77835.

This should improve performance greatly for File.Copy when copying from one APFS volume to a different file in the volume (I think it's within a volume anyway). It will clone the file which is instant instead of copying all of the bytes, and if cloning doesn't work it will fall back to the normal method of copying.

Informative:

  • Uses clonefile when possible on macOS to clone the file while still keeping file locking logic intact
  • Split common Unix logic into multiple functions that the macOS implementation uses parts of at different times
  • No shim needed since the ABI will not change. Precedent for doing it this way for macOS-specific functions: Fix setting creation date for OSX-like platforms #49555
  • Applies to all OSX-like platforms (macOS, iOS, tvOS, and Mac Catalyst), not just macOS

Todo:

  • Need to add tests to check the file is actually cloned so we know if it works or not
  • The logic in this commit has not been tested thoroughly (but it has been tested somewhat), so will wait to see the GitHub actions failures before refactoring (if needed)
  • Do we want tests copying between different "drives"? I think these would be valuable.

• Use copyfile (with COPYFILE_CLONE_FORCE) when possible on macOS to clone the file while still keeping file locking logic intact
• Split common Unix logic into multiple functions that the macOS implementation uses parts of at different times
• Add string version of ResolveLinkTarget to save the allocation since part of the code needs it
• Need to add tests to check the file is actually cloned so we know if it works or not
@ghostghost added area-System.IO community-contribution Indicates that the PR has been added by a community member labels Dec 5, 2022
@ghost

ghost commented Dec 5, 2022

Copy link
Copy Markdown

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

Issue Details

Fixes #77835.

This should improve performance greatly for File.Copy when copying from one APFS volume to a different file in the volume (I think it's within a volume anyway). It will clone the file which is instant instead of copying all of the bytes, and if cloning doesn't work it will fall back to the normal method of copying.

Informative:

  • Uses copyfile (with COPYFILE_CLONE_FORCE) when possible on macOS to clone the file while still keeping file locking logic intact
  • Split common Unix logic into multiple functions that the macOS implementation uses parts of at different times
  • No shim needed since the ABI will not change. Precedent for doing it this way for macOS-specific functions: Fix setting creation date for OSX-like platforms #49555
  • Applies to all OSX-like platforms, not just macOS

Todo:

  • Need to add tests to check the file is actually cloned so we know if it works or not
  • The logic in this commit has not been tested thoroughly (but it has been tested somewhat), so will wait to see the GitHub actions issues before refactoring (if needed)
  • Is it a problem that it could theoretically delete the destination file and then fail for some other reason, leaving the destination file deleted when it may not have done so before (realistically this is extremely unlikely)?
Author:hamarb123
Assignees:-
Labels:

area-System.IO

Milestone:-

@hamarb123

Copy link
Copy Markdown
ContributorAuthor

Btw, this implementation is not done, I want to see if any tests fail first though.

• I missed setting StringMarshalling on the LibraryImport
• I missed partial on the CopyFile method implementations
• Apparently 'Both partial method declarations must be unsafe or neither may be unsafe'
• I love partial methods
• Move unsafe keyword into the OSX method rather than in method declaration
• Fix accessibility modifier of CopyFile in FileSystem.Unix.cs
• Fix indentation (should have used spaces)
• Import Microsoft.Win32.SafeHandles in FileSystem.CopyFile.OSX.cs file
• Fix for SA1205 'Partial elements should declare an access modifier'
• Fix typo in FileSystem.CopyFile.OtherUnix.cs of 'startedCopyFile'
• Fix missing openDst parameter code in StartCopyFile
• Fix not checking the nullability in StandardCopyFile, which led to 'Possible null reference argument for parameter'
• Add missing parameter value (openNewFile) to OpenCopyFileDstHandle
• Fix misnamed variable usages throughout some code
• Properly qualify Interop.ErrorInfo
• This should be the last fix for build issues for this set
@hamarb123

Copy link
Copy Markdown
ContributorAuthor

Sorry for all of the fix commits, I couldn't get build locally to work, so I copied the code into a new project to test it and must have missed some of the changes here.

@hamarb123
hamarb123 marked this pull request as draft December 5, 2022 21:44
• Add missing nullable ? for dstHandle (which obtains the file lock)
• Test failures were since copying a file onto itself should cause an error, this fixes that
…ain)
• The variable in the if statements weren't updated
• Handling for if readlink fails
…t#77835'
• The source path should have used sourceFullPath rather than destPath
@marek-safarmarek-safar added the os-mac-os-x macOS aka OSX label Dec 6, 2022
@hamarb123

hamarb123 commented Dec 6, 2022

Copy link
Copy Markdown
ContributorAuthor

@marek-safar, this applies to macOS, iOS, tvOS, and Mac Catalyst in case you wanted to add tags for those. Can you also 'assign' me if you're able? Thanks.

@hamarb123hamarb123 changed the title [WIP] Cloning files on macOS when possible instead of copying the whole file[WIP] Cloning files on OSX-like platforms when possible instead of copying the whole fileDec 6, 2022
@ghostghost closed this Jan 6, 2023
@ghost

ghost commented Jan 6, 2023

Copy link
Copy Markdown

Draft Pull Request was automatically closed for 30 days of inactivity. Please let us know if you'd like to reopen it.

@adamsitnik

Copy link
Copy Markdown
Member

@filipnavara is there any chance you could review this PR? You are currently the most experience person in this particular area.

@hamarb123

Copy link
Copy Markdown
ContributorAuthor

Is there a reason why 24 test runs have been cancelled so far? Do the tests need to be re-run?

@danmoseley

Copy link
Copy Markdown
Contributor

@hamarb123 it is dotnet/arcade#13774

@slang25

Copy link
Copy Markdown
Contributor

I'm really looking forward to this change! Will we be seeing it land in a .NET 8 preview?

@danmoseley

Copy link
Copy Markdown
Contributor

I'm really looking forward to this change! Will we be seeing it land in a .NET 8 preview?

If it's merged in the next month or two, it will be in .NET 8.

@hamarb123

Copy link
Copy Markdown
ContributorAuthor

I'm really looking forward to this change! Will we be seeing it land in a .NET 8 preview?

It's hopefully just a few more stages of review at most. I'm hoping it will make it to .NET 8 also.

@tmds

tmds commented Jun 13, 2023

Copy link
Copy Markdown
Member

Per @adamsitnik's earlier comment, macOS isn't benchmarked in the perf lab.

So before merging, we need some benchmark results that verify there is a performance gain.

@hamarb123 can you look at the documentation Adam shared, and run the benchmarks on your machine?

@hamarb123

hamarb123 commented Jun 13, 2023

Copy link
Copy Markdown
ContributorAuthor

Benchmark results:

  • CopyTo is ~1.9x faster for small files with the changes, and is usually ~180us for all the tests up to 1MB (~207us for 100MB test). Note this test also deletes the file as a part of the test, so simply copying would likely be faster again.
  • CopyToOverwrite is ~1.15x faster for small files with the changes, and is ~215us for all the tests.
  • Copying tests in System.IO.Tests.Perf_FileStream did not improve (haven't included them here), do we want to work out why and improve that API also?
  • The comparison was between the commit just before this comment which merges all of the new runtime changes, and the runtime repo with those same runtime changes but without this PR.

System.IO.Tests.Perf_File

With changes
MethodsizeMeanErrorStdDevMedianMinMaxAllocated
CopyTo512180.4 us3.42 us3.66 us180.0 us173.9 us186.3 us65 B
CopyToOverwrite512217.4 us4.17 us4.10 us217.2 us210.2 us225.9 us129 B
CopyTo4096175.9 us3.41 us3.19 us175.0 us171.2 us182.7 us65 B
CopyToOverwrite4096214.0 us2.41 us2.13 us213.4 us209.8 us218.2 us129 B
CopyTo1048576175.9 us2.41 us2.01 us175.9 us173.4 us179.8 us65 B
CopyToOverwrite1048576212.3 us3.97 us3.31 us212.0 us208.1 us218.1 us129 B
CopyTo104857600206.9 us2.32 us1.94 us206.6 us203.7 us210.8 us65 B
CopyToOverwrite104857600213.9 us3.58 us3.17 us214.2 us209.1 us218.0 us129 B
Without changes
MethodsizeMeanErrorStdDevMedianMinMaxAllocated
CopyTo512347.6 us12.46 us14.35 us344.8 us326.4 us374.2 us129 B
CopyToOverwrite512249.2 us5.56 us6.18 us248.7 us241.4 us261.1 us129 B
CopyTo4096347.1 us7.16 us7.36 us345.2 us335.4 us364.5 us129 B
CopyToOverwrite4096244.6 us3.07 us2.72 us243.9 us240.4 us248.4 us129 B
CopyTo10485761,313.5 us68.13 us72.90 us1,322.9 us1,191.9 us1,437.3 us133 B
CopyToOverwrite10485761,773.5 us321.63 us370.39 us1,730.5 us1,184.8 us2,432.6 us136 B
CopyTo10485760055,961.7 us4,954.14 us5,705.19 us53,346.9 us49,530.1 us65,915.7 us-
CopyToOverwrite10485760055,883.4 us5,472.86 us6,302.55 us54,004.8 us46,797.4 us66,152.4 us-

@tmds

tmds commented Jun 13, 2023

Copy link
Copy Markdown
Member

CopyToOverwrite is ~1.15x faster for small files with the changes

That's a pleasant surprise.

My expectation was to see a regression for small files with overwrite since we're making additional system calls and the benefit of cloning is low for small files.

@adamsitnikadamsitnik left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The code looks very good, I found only one thing that could be improved (PTAL at my comment).

The perf win is very impressive. I've benchmarked it on my old macBook:

BenchmarkDotNet=v0.13.2.2052-nightly, OS=macOS Monterey 12.6.5 (21G531) [Darwin 21.6.0]
Intel Core i7-5557U CPU 3.10GHz (Broadwell), 1 CPU, 4 logical and 2 physical cores
.NET SDK=8.0.100-preview.6.23312.5
[Host] : .NET 8.0.0 (8.0.23.30704), X64 RyuJIT AVX2
PR : .NET 8.0.0 (42.42.42.42424), X64 RyuJIT AVX2
main : .NET 8.0.0 (8.0.23.30704), X64 RyuJIT AVX2
MethodJobsizeMeanStdDevRatio
CopyToPR512438.7 μs106.58 μs0.75
CopyTomain512588.0 μs15.62 μs1.00
CopyToOverwritePR512298.8 μs6.18 μs0.79
CopyToOverwritemain512381.4 μs4.55 μs1.00
CopyToPR4096237.0 μs6.29 μs0.39
CopyTomain4096606.9 μs37.95 μs1.00
CopyToOverwritePR4096293.7 μs7.59 μs0.76
CopyToOverwritemain4096388.1 μs3.02 μs1.00
CopyToPR1048576239.9 μs10.95 μs0.09
CopyTomain10485762,567.9 μs107.61 μs1.00
CopyToOverwritePR1048576298.2 μs11.47 μs0.12
CopyToOverwritemain10485762,585.2 μs63.77 μs1.00
CopyToPR104857600247.0 μs8.00 μs0.001
CopyTomain104857600230,797.9 μs42,923.74 μs1.000
CopyToOverwritePR104857600291.4 μs6.45 μs0.001
CopyToOverwritemain104857600256,931.8 μs2,784.23 μs1.000

Great job @hamarb123 !!

@stephentoub

stephentoub commented Jun 13, 2023

Copy link
Copy Markdown
Member

The perf win is very impressive.

While this isn't a common scenario, in the name of completeness, what does the perf look like if, after copying, the benchmark then modifies the destination file in some way?

@adamsitnik

Copy link
Copy Markdown
Member

While this isn't a common scenario, in the name of completeness, what does the perf look like if, after copying, the benchmark then modifies the destination file in some way?

That is a very good question!

I've modified the benchmarks and just added File.WriteAllBytes(dest, File.ReadAllBytes(source)) right after the call to copy:

[Benchmark]
[Arguments(HalfKibibyte)]
[Arguments(FourKibibytes)]
[Arguments(OneMibibyte)]
[Arguments(HundredMibibytes)]
public void CopyTo_AndModify(int size)
{
File.Delete(_testFilePath);
File.Copy(_filesToRead[size], _testFilePath); // overwrite defaults to false
+ File.WriteAllBytes(_testFilePath, File.ReadAllBytes(_filesToRead[size]));
}
[Benchmark]
[Arguments(HalfKibibyte)]
[Arguments(FourKibibytes)]
[Arguments(OneMibibyte)]
[Arguments(HundredMibibytes)]
public void CopyToOverwrite_AndModify(int size)
{
File.Copy(_filesToRead[size], _testFilePath, overwrite: true);
+ File.WriteAllBytes(_testFilePath, File.ReadAllBytes(_filesToRead[size]));
}

We have still a major win for large files and no regression for smaller files

MethodJobsizeMeanStdDevRatio
CopyTo_AndModifyPR512533.1 μs16.83 μs0.71
CopyTo_AndModifymain512742.4 μs6.98 μs1.00
CopyToOverwrite_AndModifyPR512588.8 μs25.28 μs0.98
CopyToOverwrite_AndModifymain512593.9 μs10.35 μs1.00
CopyTo_AndModifyPR4096540.8 μs15.53 μs0.71
CopyTo_AndModifymain4096767.0 μs13.17 μs1.00
CopyToOverwrite_AndModifyPR4096590.7 μs19.49 μs1.00
CopyToOverwrite_AndModifymain4096594.7 μs5.73 μs1.00
CopyTo_AndModifyPR10485762,383.3 μs84.83 μs0.64
CopyTo_AndModifymain10485763,701.9 μs82.21 μs1.00
CopyToOverwrite_AndModifyPR10485762,358.2 μs69.96 μs0.69
CopyToOverwrite_AndModifymain10485763,412.3 μs87.54 μs1.00
CopyTo_AndModifyPR104857600167,426.8 μs18,209.30 μs0.61
CopyTo_AndModifymain104857600276,862.8 μs29,743.99 μs1.00
CopyToOverwrite_AndModifyPR104857600170,189.9 μs26,538.74 μs0.61
CopyToOverwrite_AndModifymain104857600287,618.2 μs48,952.24 μs1.00

@stephentoub

Copy link
Copy Markdown
Member

File.WriteAllBytes

WriteAllBytes opens the file as Create, which means it's going to effectively delete the destination and write a new file rather than modify the one that's there.

@adamsitnik

Copy link
Copy Markdown
Member

WriteAllBytes opens the file as Create, which means it's going to effectively delete the destination and write a new file rather than modify the one that's there.

Sorry, I forgot about that (BTW it just proves that we should really add #84532)

Updated benchmark:

privatebyte[]_oneKb=newbyte[1024];[Benchmark][Arguments(HalfKibibyte)][Arguments(FourKibibytes)][Arguments(OneMibibyte)][Arguments(HundredMibibytes)]publicvoidCopyTo_AndModify(intsize){File.Delete(_testFilePath);File.Copy(_filesToRead[size],_testFilePath);// overwrite defaults to falseusingFileStreamfs=File.OpenWrite(_testFilePath);fs.Write(_oneKb);}[Benchmark][Arguments(HalfKibibyte)][Arguments(FourKibibytes)][Arguments(OneMibibyte)][Arguments(HundredMibibytes)]publicvoidCopyToOverwrite_AndModify(intsize){File.Copy(_filesToRead[size],_testFilePath,overwrite:true);usingFileStreamfs=File.OpenWrite(_testFilePath);fs.Write(_oneKb);}

And the results:

BenchmarkDotNet=v0.13.2.2052-nightly, OS=macOS Monterey 12.6.5 (21G531) [Darwin 21.6.0]
Intel Core i7-5557U CPU 3.10GHz (Broadwell), 1 CPU, 4 logical and 2 physical cores
.NET SDK=8.0.100-preview.6.23312.5
[Host] : .NET 8.0.0 (8.0.23.30704), X64 RyuJIT AVX2
PR : .NET 8.0.0 (42.42.42.42424), X64 RyuJIT AVX2
main : .NET 8.0.0 (8.0.23.30704), X64 RyuJIT AVX2
IterationCount=40
MethodJobsizeMeanStdDevRatio
CopyTo_AndModifyPR512537.7 μs13.18 μs0.82
CopyTo_AndModifymain512661.3 μs9.98 μs1.00
CopyToOverwrite_AndModifyPR512549.1 μs16.54 μs1.15
CopyToOverwrite_AndModifymain512476.3 μs7.58 μs1.00
CopyTo_AndModifyPR4096669.8 μs20.57 μs1.01
CopyTo_AndModifymain4096662.4 μs11.60 μs1.00
CopyToOverwrite_AndModifyPR4096695.3 μs26.41 μs1.50
CopyToOverwrite_AndModifymain4096465.0 μs4.94 μs1.00
CopyTo_AndModifyPR1048576722.5 μs14.36 μs0.35
CopyTo_AndModifymain10485762,051.8 μs40.83 μs1.00
CopyToOverwrite_AndModifyPR1048576728.4 μs34.28 μs0.37
CopyToOverwrite_AndModifymain10485761,991.1 μs163.53 μs1.00
CopyTo_AndModifyPR104857600804.9 μs18.60 μs0.007
CopyTo_AndModifymain104857600109,415.3 μs8,542.98 μs1.000
CopyToOverwrite_AndModifyPR104857600762.1 μs26.40 μs0.007
CopyToOverwrite_AndModifymain104857600117,399.2 μs16,068.51 μs1.000

@adamsitnik
adamsitnik merged commit 8fb25a8 into dotnet:mainJun 13, 2023
@adamsitnikadamsitnik added this to the 8.0.0 milestone Jun 13, 2023
@stephentoub

Copy link
Copy Markdown
Member

BTW it just proves that we should really add #84532

Not really 😄

Updated benchmark

Thanks. So this does regress small overwrites that are subsequently modified.

@adamsitnik

Copy link
Copy Markdown
Member

Not really

Why? Do you believe that appending a buffer of bytes to an existing file is a rare scenario?

@danmoseley

Copy link
Copy Markdown
Contributor

Thanks for this nice change @hamarb123 . Would you like to tackle another issue? There are also a number marked api-approved that are available.

@stephentoub

stephentoub commented Jun 13, 2023

Copy link
Copy Markdown
Member

BTW it just proves that we should really add #84532

Not really

Why? Do you believe that appending a buffer of bytes to an existing file is a rare scenario?

I'm not commenting on the scenario of appending a buffer of bytes to an existing file. I'm commenting that a need to mutate a recently copied file as part of benchmarking a copy-on-write optimization and to achieve that writing two short lines of code doesn't prove that we need to add a new API.

@hamarb123

Copy link
Copy Markdown
ContributorAuthor

Thanks for this nice change @hamarb123 . Would you like to tackle another issue? There are also a number marked api-approved that are available.

Thanks! I'll have a look tomorrow (also known as a today at a sensible hour lol). I was also going to do the one for ReFS on Windows, was setting up a VM to test dev drives yesterday.

Copying tests in System.IO.Tests.Perf_FileStream did not improve (haven't included them here), do we want to work out why and improve that API also?

Just following up on this also. Presumably this API just does a block copy with the files. Is this something that's commonly used (I know I've never done it like this)? Because it doesn't seem to benefit from this, but maybe no-one uses it, idk.

@adamsitnik

Copy link
Copy Markdown
Member

Copying tests in System.IO.Tests.Perf_FileStream did not improve

IIRC The FileStream.CopyTo(Stream destination) has no special handling for cases where the destination stream is also a file stream.

Is this something that's commonly used

I don't have enough data to answer your question, but if you can provide an optimization that improves such scenario without introducing a lot of code complexity I am happy to review it.

@danmoseley

Copy link
Copy Markdown
Contributor

I was also going to do the one for ReFS on Windows

Ah yes, indeed that would be nice to get into .NET 8

@ghostghost locked as resolved and limited conversation to collaborators Jul 15, 2023
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-System.IOcommunity-contributionIndicates that the PR has been added by a community memberos-mac-os-xmacOS aka OSX

Projects

None yet

Development

Successfully merging this pull request may close these issues.

File.Copy: Clone file when possible on MacOS

10 participants

@hamarb123@adamsitnik@filipnavara@akoeplinger@stephentoub@danmoseley@jkotas@tmds@slang25@marek-safar
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Clone files on OSX-like platforms when possible, instead of copying the whole file by hamarb123 · Pull Request #79243 · dotnet/runtime · GitHub
Skip to content

Clone files on OSX-like platforms when possible, instead of copying the whole file - #79243

Merged
adamsitnik merged 85 commits into
dotnet:mainfrom
hamarb123:main
Jun 13, 2023
Merged

Clone files on OSX-like platforms when possible, instead of copying the whole file#79243
adamsitnik merged 85 commits into
dotnet:mainfrom
hamarb123:main

Conversation

@hamarb123

@hamarb123hamarb123 commented Dec 5, 2022

Copy link
Copy Markdown
Contributor

Fixes#77835.

This should improve performance greatly for File.Copy when copying from one APFS volume to a different file in the volume (I think it's within a volume anyway). It will clone the file which is instant instead of copying all of the bytes, and if cloning doesn't work it will fall back to the normal method of copying.

Informative:

  • Uses clonefile when possible on macOS to clone the file while still keeping file locking logic intact
  • Split common Unix logic into multiple functions that the macOS implementation uses parts of at different times
  • No shim needed since the ABI will not change. Precedent for doing it this way for macOS-specific functions: Fix setting creation date for OSX-like platforms #49555
  • Applies to all OSX-like platforms (macOS, iOS, tvOS, and Mac Catalyst), not just macOS

Todo:

  • Need to add tests to check the file is actually cloned so we know if it works or not
  • The logic in this commit has not been tested thoroughly (but it has been tested somewhat), so will wait to see the GitHub actions failures before refactoring (if needed)
  • Do we want tests copying between different "drives"? I think these would be valuable.

• Use copyfile (with COPYFILE_CLONE_FORCE) when possible on macOS to clone the file while still keeping file locking logic intact
• Split common Unix logic into multiple functions that the macOS implementation uses parts of at different times
• Add string version of ResolveLinkTarget to save the allocation since part of the code needs it
• Need to add tests to check the file is actually cloned so we know if it works or not
@ghostghost added area-System.IO community-contribution Indicates that the PR has been added by a community member labels Dec 5, 2022
@ghost

ghost commented Dec 5, 2022

Copy link
Copy Markdown

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

Issue Details

Fixes #77835.

This should improve performance greatly for File.Copy when copying from one APFS volume to a different file in the volume (I think it's within a volume anyway). It will clone the file which is instant instead of copying all of the bytes, and if cloning doesn't work it will fall back to the normal method of copying.

Informative:

  • Uses copyfile (with COPYFILE_CLONE_FORCE) when possible on macOS to clone the file while still keeping file locking logic intact
  • Split common Unix logic into multiple functions that the macOS implementation uses parts of at different times
  • No shim needed since the ABI will not change. Precedent for doing it this way for macOS-specific functions: Fix setting creation date for OSX-like platforms #49555
  • Applies to all OSX-like platforms, not just macOS

Todo:

  • Need to add tests to check the file is actually cloned so we know if it works or not
  • The logic in this commit has not been tested thoroughly (but it has been tested somewhat), so will wait to see the GitHub actions issues before refactoring (if needed)
  • Is it a problem that it could theoretically delete the destination file and then fail for some other reason, leaving the destination file deleted when it may not have done so before (realistically this is extremely unlikely)?
Author:hamarb123
Assignees:-
Labels:

area-System.IO

Milestone:-

@hamarb123

Copy link
Copy Markdown
ContributorAuthor

Btw, this implementation is not done, I want to see if any tests fail first though.

• I missed setting StringMarshalling on the LibraryImport
• I missed partial on the CopyFile method implementations
• Apparently 'Both partial method declarations must be unsafe or neither may be unsafe'
• I love partial methods
• Move unsafe keyword into the OSX method rather than in method declaration
• Fix accessibility modifier of CopyFile in FileSystem.Unix.cs
• Fix indentation (should have used spaces)
• Import Microsoft.Win32.SafeHandles in FileSystem.CopyFile.OSX.cs file
• Fix for SA1205 'Partial elements should declare an access modifier'
• Fix typo in FileSystem.CopyFile.OtherUnix.cs of 'startedCopyFile'
• Fix missing openDst parameter code in StartCopyFile
• Fix not checking the nullability in StandardCopyFile, which led to 'Possible null reference argument for parameter'
• Add missing parameter value (openNewFile) to OpenCopyFileDstHandle
• Fix misnamed variable usages throughout some code
• Properly qualify Interop.ErrorInfo
• This should be the last fix for build issues for this set
@hamarb123

Copy link
Copy Markdown
ContributorAuthor

Sorry for all of the fix commits, I couldn't get build locally to work, so I copied the code into a new project to test it and must have missed some of the changes here.

@hamarb123
hamarb123 marked this pull request as draft December 5, 2022 21:44
• Add missing nullable ? for dstHandle (which obtains the file lock)
• Test failures were since copying a file onto itself should cause an error, this fixes that
…ain)
• The variable in the if statements weren't updated
• Handling for if readlink fails
…t#77835'
• The source path should have used sourceFullPath rather than destPath
@marek-safarmarek-safar added the os-mac-os-x macOS aka OSX label Dec 6, 2022
@hamarb123

hamarb123 commented Dec 6, 2022

Copy link
Copy Markdown
ContributorAuthor

@marek-safar, this applies to macOS, iOS, tvOS, and Mac Catalyst in case you wanted to add tags for those. Can you also 'assign' me if you're able? Thanks.

@hamarb123hamarb123 changed the title [WIP] Cloning files on macOS when possible instead of copying the whole file[WIP] Cloning files on OSX-like platforms when possible instead of copying the whole fileDec 6, 2022
@ghostghost closed this Jan 6, 2023
@ghost

ghost commented Jan 6, 2023

Copy link
Copy Markdown

Draft Pull Request was automatically closed for 30 days of inactivity. Please let us know if you'd like to reopen it.

@adamsitnik

Copy link
Copy Markdown
Member

@filipnavara is there any chance you could review this PR? You are currently the most experience person in this particular area.

@hamarb123

Copy link
Copy Markdown
ContributorAuthor

Is there a reason why 24 test runs have been cancelled so far? Do the tests need to be re-run?

@danmoseley

Copy link
Copy Markdown
Contributor

@hamarb123 it is dotnet/arcade#13774

@slang25

Copy link
Copy Markdown
Contributor

I'm really looking forward to this change! Will we be seeing it land in a .NET 8 preview?

@danmoseley

Copy link
Copy Markdown
Contributor

I'm really looking forward to this change! Will we be seeing it land in a .NET 8 preview?

If it's merged in the next month or two, it will be in .NET 8.

@hamarb123

Copy link
Copy Markdown
ContributorAuthor

I'm really looking forward to this change! Will we be seeing it land in a .NET 8 preview?

It's hopefully just a few more stages of review at most. I'm hoping it will make it to .NET 8 also.

@tmds

tmds commented Jun 13, 2023

Copy link
Copy Markdown
Member

Per @adamsitnik's earlier comment, macOS isn't benchmarked in the perf lab.

So before merging, we need some benchmark results that verify there is a performance gain.

@hamarb123 can you look at the documentation Adam shared, and run the benchmarks on your machine?

@hamarb123

hamarb123 commented Jun 13, 2023

Copy link
Copy Markdown
ContributorAuthor

Benchmark results:

  • CopyTo is ~1.9x faster for small files with the changes, and is usually ~180us for all the tests up to 1MB (~207us for 100MB test). Note this test also deletes the file as a part of the test, so simply copying would likely be faster again.
  • CopyToOverwrite is ~1.15x faster for small files with the changes, and is ~215us for all the tests.
  • Copying tests in System.IO.Tests.Perf_FileStream did not improve (haven't included them here), do we want to work out why and improve that API also?
  • The comparison was between the commit just before this comment which merges all of the new runtime changes, and the runtime repo with those same runtime changes but without this PR.

System.IO.Tests.Perf_File

With changes
MethodsizeMeanErrorStdDevMedianMinMaxAllocated
CopyTo512180.4 us3.42 us3.66 us180.0 us173.9 us186.3 us65 B
CopyToOverwrite512217.4 us4.17 us4.10 us217.2 us210.2 us225.9 us129 B
CopyTo4096175.9 us3.41 us3.19 us175.0 us171.2 us182.7 us65 B
CopyToOverwrite4096214.0 us2.41 us2.13 us213.4 us209.8 us218.2 us129 B
CopyTo1048576175.9 us2.41 us2.01 us175.9 us173.4 us179.8 us65 B
CopyToOverwrite1048576212.3 us3.97 us3.31 us212.0 us208.1 us218.1 us129 B
CopyTo104857600206.9 us2.32 us1.94 us206.6 us203.7 us210.8 us65 B
CopyToOverwrite104857600213.9 us3.58 us3.17 us214.2 us209.1 us218.0 us129 B
Without changes
MethodsizeMeanErrorStdDevMedianMinMaxAllocated
CopyTo512347.6 us12.46 us14.35 us344.8 us326.4 us374.2 us129 B
CopyToOverwrite512249.2 us5.56 us6.18 us248.7 us241.4 us261.1 us129 B
CopyTo4096347.1 us7.16 us7.36 us345.2 us335.4 us364.5 us129 B
CopyToOverwrite4096244.6 us3.07 us2.72 us243.9 us240.4 us248.4 us129 B
CopyTo10485761,313.5 us68.13 us72.90 us1,322.9 us1,191.9 us1,437.3 us133 B
CopyToOverwrite10485761,773.5 us321.63 us370.39 us1,730.5 us1,184.8 us2,432.6 us136 B
CopyTo10485760055,961.7 us4,954.14 us5,705.19 us53,346.9 us49,530.1 us65,915.7 us-
CopyToOverwrite10485760055,883.4 us5,472.86 us6,302.55 us54,004.8 us46,797.4 us66,152.4 us-

@tmds

tmds commented Jun 13, 2023

Copy link
Copy Markdown
Member

CopyToOverwrite is ~1.15x faster for small files with the changes

That's a pleasant surprise.

My expectation was to see a regression for small files with overwrite since we're making additional system calls and the benefit of cloning is low for small files.

@adamsitnikadamsitnik left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The code looks very good, I found only one thing that could be improved (PTAL at my comment).

The perf win is very impressive. I've benchmarked it on my old macBook:

BenchmarkDotNet=v0.13.2.2052-nightly, OS=macOS Monterey 12.6.5 (21G531) [Darwin 21.6.0]
Intel Core i7-5557U CPU 3.10GHz (Broadwell), 1 CPU, 4 logical and 2 physical cores
.NET SDK=8.0.100-preview.6.23312.5
[Host] : .NET 8.0.0 (8.0.23.30704), X64 RyuJIT AVX2
PR : .NET 8.0.0 (42.42.42.42424), X64 RyuJIT AVX2
main : .NET 8.0.0 (8.0.23.30704), X64 RyuJIT AVX2
MethodJobsizeMeanStdDevRatio
CopyToPR512438.7 μs106.58 μs0.75
CopyTomain512588.0 μs15.62 μs1.00
CopyToOverwritePR512298.8 μs6.18 μs0.79
CopyToOverwritemain512381.4 μs4.55 μs1.00
CopyToPR4096237.0 μs6.29 μs0.39
CopyTomain4096606.9 μs37.95 μs1.00
CopyToOverwritePR4096293.7 μs7.59 μs0.76
CopyToOverwritemain4096388.1 μs3.02 μs1.00
CopyToPR1048576239.9 μs10.95 μs0.09
CopyTomain10485762,567.9 μs107.61 μs1.00
CopyToOverwritePR1048576298.2 μs11.47 μs0.12
CopyToOverwritemain10485762,585.2 μs63.77 μs1.00
CopyToPR104857600247.0 μs8.00 μs0.001
CopyTomain104857600230,797.9 μs42,923.74 μs1.000
CopyToOverwritePR104857600291.4 μs6.45 μs0.001
CopyToOverwritemain104857600256,931.8 μs2,784.23 μs1.000

Great job @hamarb123 !!

@stephentoub

stephentoub commented Jun 13, 2023

Copy link
Copy Markdown
Member

The perf win is very impressive.

While this isn't a common scenario, in the name of completeness, what does the perf look like if, after copying, the benchmark then modifies the destination file in some way?

@adamsitnik

Copy link
Copy Markdown
Member

While this isn't a common scenario, in the name of completeness, what does the perf look like if, after copying, the benchmark then modifies the destination file in some way?

That is a very good question!

I've modified the benchmarks and just added File.WriteAllBytes(dest, File.ReadAllBytes(source)) right after the call to copy:

[Benchmark]
[Arguments(HalfKibibyte)]
[Arguments(FourKibibytes)]
[Arguments(OneMibibyte)]
[Arguments(HundredMibibytes)]
public void CopyTo_AndModify(int size)
{
File.Delete(_testFilePath);
File.Copy(_filesToRead[size], _testFilePath); // overwrite defaults to false
+ File.WriteAllBytes(_testFilePath, File.ReadAllBytes(_filesToRead[size]));
}
[Benchmark]
[Arguments(HalfKibibyte)]
[Arguments(FourKibibytes)]
[Arguments(OneMibibyte)]
[Arguments(HundredMibibytes)]
public void CopyToOverwrite_AndModify(int size)
{
File.Copy(_filesToRead[size], _testFilePath, overwrite: true);
+ File.WriteAllBytes(_testFilePath, File.ReadAllBytes(_filesToRead[size]));
}

We have still a major win for large files and no regression for smaller files

MethodJobsizeMeanStdDevRatio
CopyTo_AndModifyPR512533.1 μs16.83 μs0.71
CopyTo_AndModifymain512742.4 μs6.98 μs1.00
CopyToOverwrite_AndModifyPR512588.8 μs25.28 μs0.98
CopyToOverwrite_AndModifymain512593.9 μs10.35 μs1.00
CopyTo_AndModifyPR4096540.8 μs15.53 μs0.71
CopyTo_AndModifymain4096767.0 μs13.17 μs1.00
CopyToOverwrite_AndModifyPR4096590.7 μs19.49 μs1.00
CopyToOverwrite_AndModifymain4096594.7 μs5.73 μs1.00
CopyTo_AndModifyPR10485762,383.3 μs84.83 μs0.64
CopyTo_AndModifymain10485763,701.9 μs82.21 μs1.00
CopyToOverwrite_AndModifyPR10485762,358.2 μs69.96 μs0.69
CopyToOverwrite_AndModifymain10485763,412.3 μs87.54 μs1.00
CopyTo_AndModifyPR104857600167,426.8 μs18,209.30 μs0.61
CopyTo_AndModifymain104857600276,862.8 μs29,743.99 μs1.00
CopyToOverwrite_AndModifyPR104857600170,189.9 μs26,538.74 μs0.61
CopyToOverwrite_AndModifymain104857600287,618.2 μs48,952.24 μs1.00

@stephentoub

Copy link
Copy Markdown
Member

File.WriteAllBytes

WriteAllBytes opens the file as Create, which means it's going to effectively delete the destination and write a new file rather than modify the one that's there.

@adamsitnik

Copy link
Copy Markdown
Member

WriteAllBytes opens the file as Create, which means it's going to effectively delete the destination and write a new file rather than modify the one that's there.

Sorry, I forgot about that (BTW it just proves that we should really add #84532)

Updated benchmark:

privatebyte[]_oneKb=newbyte[1024];[Benchmark][Arguments(HalfKibibyte)][Arguments(FourKibibytes)][Arguments(OneMibibyte)][Arguments(HundredMibibytes)]publicvoidCopyTo_AndModify(intsize){File.Delete(_testFilePath);File.Copy(_filesToRead[size],_testFilePath);// overwrite defaults to falseusingFileStreamfs=File.OpenWrite(_testFilePath);fs.Write(_oneKb);}[Benchmark][Arguments(HalfKibibyte)][Arguments(FourKibibytes)][Arguments(OneMibibyte)][Arguments(HundredMibibytes)]publicvoidCopyToOverwrite_AndModify(intsize){File.Copy(_filesToRead[size],_testFilePath,overwrite:true);usingFileStreamfs=File.OpenWrite(_testFilePath);fs.Write(_oneKb);}

And the results:

BenchmarkDotNet=v0.13.2.2052-nightly, OS=macOS Monterey 12.6.5 (21G531) [Darwin 21.6.0]
Intel Core i7-5557U CPU 3.10GHz (Broadwell), 1 CPU, 4 logical and 2 physical cores
.NET SDK=8.0.100-preview.6.23312.5
[Host] : .NET 8.0.0 (8.0.23.30704), X64 RyuJIT AVX2
PR : .NET 8.0.0 (42.42.42.42424), X64 RyuJIT AVX2
main : .NET 8.0.0 (8.0.23.30704), X64 RyuJIT AVX2
IterationCount=40
MethodJobsizeMeanStdDevRatio
CopyTo_AndModifyPR512537.7 μs13.18 μs0.82
CopyTo_AndModifymain512661.3 μs9.98 μs1.00
CopyToOverwrite_AndModifyPR512549.1 μs16.54 μs1.15
CopyToOverwrite_AndModifymain512476.3 μs7.58 μs1.00
CopyTo_AndModifyPR4096669.8 μs20.57 μs1.01
CopyTo_AndModifymain4096662.4 μs11.60 μs1.00
CopyToOverwrite_AndModifyPR4096695.3 μs26.41 μs1.50
CopyToOverwrite_AndModifymain4096465.0 μs4.94 μs1.00
CopyTo_AndModifyPR1048576722.5 μs14.36 μs0.35
CopyTo_AndModifymain10485762,051.8 μs40.83 μs1.00
CopyToOverwrite_AndModifyPR1048576728.4 μs34.28 μs0.37
CopyToOverwrite_AndModifymain10485761,991.1 μs163.53 μs1.00
CopyTo_AndModifyPR104857600804.9 μs18.60 μs0.007
CopyTo_AndModifymain104857600109,415.3 μs8,542.98 μs1.000
CopyToOverwrite_AndModifyPR104857600762.1 μs26.40 μs0.007
CopyToOverwrite_AndModifymain104857600117,399.2 μs16,068.51 μs1.000

@adamsitnik
adamsitnik merged commit 8fb25a8 into dotnet:mainJun 13, 2023
@adamsitnikadamsitnik added this to the 8.0.0 milestone Jun 13, 2023
@stephentoub

Copy link
Copy Markdown
Member

BTW it just proves that we should really add #84532

Not really 😄

Updated benchmark

Thanks. So this does regress small overwrites that are subsequently modified.

@adamsitnik

Copy link
Copy Markdown
Member

Not really

Why? Do you believe that appending a buffer of bytes to an existing file is a rare scenario?

@danmoseley

Copy link
Copy Markdown
Contributor

Thanks for this nice change @hamarb123 . Would you like to tackle another issue? There are also a number marked api-approved that are available.

@stephentoub

stephentoub commented Jun 13, 2023

Copy link
Copy Markdown
Member

BTW it just proves that we should really add #84532

Not really

Why? Do you believe that appending a buffer of bytes to an existing file is a rare scenario?

I'm not commenting on the scenario of appending a buffer of bytes to an existing file. I'm commenting that a need to mutate a recently copied file as part of benchmarking a copy-on-write optimization and to achieve that writing two short lines of code doesn't prove that we need to add a new API.

@hamarb123

Copy link
Copy Markdown
ContributorAuthor

Thanks for this nice change @hamarb123 . Would you like to tackle another issue? There are also a number marked api-approved that are available.

Thanks! I'll have a look tomorrow (also known as a today at a sensible hour lol). I was also going to do the one for ReFS on Windows, was setting up a VM to test dev drives yesterday.

Copying tests in System.IO.Tests.Perf_FileStream did not improve (haven't included them here), do we want to work out why and improve that API also?

Just following up on this also. Presumably this API just does a block copy with the files. Is this something that's commonly used (I know I've never done it like this)? Because it doesn't seem to benefit from this, but maybe no-one uses it, idk.

@adamsitnik

Copy link
Copy Markdown
Member

Copying tests in System.IO.Tests.Perf_FileStream did not improve

IIRC The FileStream.CopyTo(Stream destination) has no special handling for cases where the destination stream is also a file stream.

Is this something that's commonly used

I don't have enough data to answer your question, but if you can provide an optimization that improves such scenario without introducing a lot of code complexity I am happy to review it.

@danmoseley

Copy link
Copy Markdown
Contributor

I was also going to do the one for ReFS on Windows

Ah yes, indeed that would be nice to get into .NET 8

@ghostghost locked as resolved and limited conversation to collaborators Jul 15, 2023
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-System.IOcommunity-contributionIndicates that the PR has been added by a community memberos-mac-os-xmacOS aka OSX

Projects

None yet

Development

Successfully merging this pull request may close these issues.

File.Copy: Clone file when possible on MacOS

10 participants

@hamarb123@adamsitnik@filipnavara@akoeplinger@stephentoub@danmoseley@jkotas@tmds@slang25@marek-safar
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Clone files on OSX-like platforms when possible, instead of copying the whole file by hamarb123 · Pull Request #79243 · dotnet/runtime · GitHub
Skip to content

Clone files on OSX-like platforms when possible, instead of copying the whole file - #79243

Merged
adamsitnik merged 85 commits into
dotnet:mainfrom
hamarb123:main
Jun 13, 2023
Merged

Clone files on OSX-like platforms when possible, instead of copying the whole file#79243
adamsitnik merged 85 commits into
dotnet:mainfrom
hamarb123:main

Conversation

@hamarb123

@hamarb123hamarb123 commented Dec 5, 2022

Copy link
Copy Markdown
Contributor

Fixes#77835.

This should improve performance greatly for File.Copy when copying from one APFS volume to a different file in the volume (I think it's within a volume anyway). It will clone the file which is instant instead of copying all of the bytes, and if cloning doesn't work it will fall back to the normal method of copying.

Informative:

  • Uses clonefile when possible on macOS to clone the file while still keeping file locking logic intact
  • Split common Unix logic into multiple functions that the macOS implementation uses parts of at different times
  • No shim needed since the ABI will not change. Precedent for doing it this way for macOS-specific functions: Fix setting creation date for OSX-like platforms #49555
  • Applies to all OSX-like platforms (macOS, iOS, tvOS, and Mac Catalyst), not just macOS

Todo:

  • Need to add tests to check the file is actually cloned so we know if it works or not
  • The logic in this commit has not been tested thoroughly (but it has been tested somewhat), so will wait to see the GitHub actions failures before refactoring (if needed)
  • Do we want tests copying between different "drives"? I think these would be valuable.

• Use copyfile (with COPYFILE_CLONE_FORCE) when possible on macOS to clone the file while still keeping file locking logic intact
• Split common Unix logic into multiple functions that the macOS implementation uses parts of at different times
• Add string version of ResolveLinkTarget to save the allocation since part of the code needs it
• Need to add tests to check the file is actually cloned so we know if it works or not
@ghostghost added area-System.IO community-contribution Indicates that the PR has been added by a community member labels Dec 5, 2022
@ghost

ghost commented Dec 5, 2022

Copy link
Copy Markdown

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

Issue Details

Fixes #77835.

This should improve performance greatly for File.Copy when copying from one APFS volume to a different file in the volume (I think it's within a volume anyway). It will clone the file which is instant instead of copying all of the bytes, and if cloning doesn't work it will fall back to the normal method of copying.

Informative:

  • Uses copyfile (with COPYFILE_CLONE_FORCE) when possible on macOS to clone the file while still keeping file locking logic intact
  • Split common Unix logic into multiple functions that the macOS implementation uses parts of at different times
  • No shim needed since the ABI will not change. Precedent for doing it this way for macOS-specific functions: Fix setting creation date for OSX-like platforms #49555
  • Applies to all OSX-like platforms, not just macOS

Todo:

  • Need to add tests to check the file is actually cloned so we know if it works or not
  • The logic in this commit has not been tested thoroughly (but it has been tested somewhat), so will wait to see the GitHub actions issues before refactoring (if needed)
  • Is it a problem that it could theoretically delete the destination file and then fail for some other reason, leaving the destination file deleted when it may not have done so before (realistically this is extremely unlikely)?
Author:hamarb123
Assignees:-
Labels:

area-System.IO

Milestone:-

@hamarb123

Copy link
Copy Markdown
ContributorAuthor

Btw, this implementation is not done, I want to see if any tests fail first though.

• I missed setting StringMarshalling on the LibraryImport
• I missed partial on the CopyFile method implementations
• Apparently 'Both partial method declarations must be unsafe or neither may be unsafe'
• I love partial methods
• Move unsafe keyword into the OSX method rather than in method declaration
• Fix accessibility modifier of CopyFile in FileSystem.Unix.cs
• Fix indentation (should have used spaces)
• Import Microsoft.Win32.SafeHandles in FileSystem.CopyFile.OSX.cs file
• Fix for SA1205 'Partial elements should declare an access modifier'
• Fix typo in FileSystem.CopyFile.OtherUnix.cs of 'startedCopyFile'
• Fix missing openDst parameter code in StartCopyFile
• Fix not checking the nullability in StandardCopyFile, which led to 'Possible null reference argument for parameter'
• Add missing parameter value (openNewFile) to OpenCopyFileDstHandle
• Fix misnamed variable usages throughout some code
• Properly qualify Interop.ErrorInfo
• This should be the last fix for build issues for this set
@hamarb123

Copy link
Copy Markdown
ContributorAuthor

Sorry for all of the fix commits, I couldn't get build locally to work, so I copied the code into a new project to test it and must have missed some of the changes here.

@hamarb123
hamarb123 marked this pull request as draft December 5, 2022 21:44
• Add missing nullable ? for dstHandle (which obtains the file lock)
• Test failures were since copying a file onto itself should cause an error, this fixes that
…ain)
• The variable in the if statements weren't updated
• Handling for if readlink fails
…t#77835'
• The source path should have used sourceFullPath rather than destPath
@marek-safarmarek-safar added the os-mac-os-x macOS aka OSX label Dec 6, 2022
@hamarb123

hamarb123 commented Dec 6, 2022

Copy link
Copy Markdown
ContributorAuthor

@marek-safar, this applies to macOS, iOS, tvOS, and Mac Catalyst in case you wanted to add tags for those. Can you also 'assign' me if you're able? Thanks.

@hamarb123hamarb123 changed the title [WIP] Cloning files on macOS when possible instead of copying the whole file[WIP] Cloning files on OSX-like platforms when possible instead of copying the whole fileDec 6, 2022
@ghostghost closed this Jan 6, 2023
@ghost

ghost commented Jan 6, 2023

Copy link
Copy Markdown

Draft Pull Request was automatically closed for 30 days of inactivity. Please let us know if you'd like to reopen it.

@adamsitnik

Copy link
Copy Markdown
Member

@filipnavara is there any chance you could review this PR? You are currently the most experience person in this particular area.

@hamarb123

Copy link
Copy Markdown
ContributorAuthor

Is there a reason why 24 test runs have been cancelled so far? Do the tests need to be re-run?

@danmoseley

Copy link
Copy Markdown
Contributor

@hamarb123 it is dotnet/arcade#13774

@slang25

Copy link
Copy Markdown
Contributor

I'm really looking forward to this change! Will we be seeing it land in a .NET 8 preview?

@danmoseley

Copy link
Copy Markdown
Contributor

I'm really looking forward to this change! Will we be seeing it land in a .NET 8 preview?

If it's merged in the next month or two, it will be in .NET 8.

@hamarb123

Copy link
Copy Markdown
ContributorAuthor

I'm really looking forward to this change! Will we be seeing it land in a .NET 8 preview?

It's hopefully just a few more stages of review at most. I'm hoping it will make it to .NET 8 also.

@tmds

tmds commented Jun 13, 2023

Copy link
Copy Markdown
Member

Per @adamsitnik's earlier comment, macOS isn't benchmarked in the perf lab.

So before merging, we need some benchmark results that verify there is a performance gain.

@hamarb123 can you look at the documentation Adam shared, and run the benchmarks on your machine?

@hamarb123

hamarb123 commented Jun 13, 2023

Copy link
Copy Markdown
ContributorAuthor

Benchmark results:

  • CopyTo is ~1.9x faster for small files with the changes, and is usually ~180us for all the tests up to 1MB (~207us for 100MB test). Note this test also deletes the file as a part of the test, so simply copying would likely be faster again.
  • CopyToOverwrite is ~1.15x faster for small files with the changes, and is ~215us for all the tests.
  • Copying tests in System.IO.Tests.Perf_FileStream did not improve (haven't included them here), do we want to work out why and improve that API also?
  • The comparison was between the commit just before this comment which merges all of the new runtime changes, and the runtime repo with those same runtime changes but without this PR.

System.IO.Tests.Perf_File

With changes
MethodsizeMeanErrorStdDevMedianMinMaxAllocated
CopyTo512180.4 us3.42 us3.66 us180.0 us173.9 us186.3 us65 B
CopyToOverwrite512217.4 us4.17 us4.10 us217.2 us210.2 us225.9 us129 B
CopyTo4096175.9 us3.41 us3.19 us175.0 us171.2 us182.7 us65 B
CopyToOverwrite4096214.0 us2.41 us2.13 us213.4 us209.8 us218.2 us129 B
CopyTo1048576175.9 us2.41 us2.01 us175.9 us173.4 us179.8 us65 B
CopyToOverwrite1048576212.3 us3.97 us3.31 us212.0 us208.1 us218.1 us129 B
CopyTo104857600206.9 us2.32 us1.94 us206.6 us203.7 us210.8 us65 B
CopyToOverwrite104857600213.9 us3.58 us3.17 us214.2 us209.1 us218.0 us129 B
Without changes
MethodsizeMeanErrorStdDevMedianMinMaxAllocated
CopyTo512347.6 us12.46 us14.35 us344.8 us326.4 us374.2 us129 B
CopyToOverwrite512249.2 us5.56 us6.18 us248.7 us241.4 us261.1 us129 B
CopyTo4096347.1 us7.16 us7.36 us345.2 us335.4 us364.5 us129 B
CopyToOverwrite4096244.6 us3.07 us2.72 us243.9 us240.4 us248.4 us129 B
CopyTo10485761,313.5 us68.13 us72.90 us1,322.9 us1,191.9 us1,437.3 us133 B
CopyToOverwrite10485761,773.5 us321.63 us370.39 us1,730.5 us1,184.8 us2,432.6 us136 B
CopyTo10485760055,961.7 us4,954.14 us5,705.19 us53,346.9 us49,530.1 us65,915.7 us-
CopyToOverwrite10485760055,883.4 us5,472.86 us6,302.55 us54,004.8 us46,797.4 us66,152.4 us-

@tmds

tmds commented Jun 13, 2023

Copy link
Copy Markdown
Member

CopyToOverwrite is ~1.15x faster for small files with the changes

That's a pleasant surprise.

My expectation was to see a regression for small files with overwrite since we're making additional system calls and the benefit of cloning is low for small files.

@adamsitnikadamsitnik left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The code looks very good, I found only one thing that could be improved (PTAL at my comment).

The perf win is very impressive. I've benchmarked it on my old macBook:

BenchmarkDotNet=v0.13.2.2052-nightly, OS=macOS Monterey 12.6.5 (21G531) [Darwin 21.6.0]
Intel Core i7-5557U CPU 3.10GHz (Broadwell), 1 CPU, 4 logical and 2 physical cores
.NET SDK=8.0.100-preview.6.23312.5
[Host] : .NET 8.0.0 (8.0.23.30704), X64 RyuJIT AVX2
PR : .NET 8.0.0 (42.42.42.42424), X64 RyuJIT AVX2
main : .NET 8.0.0 (8.0.23.30704), X64 RyuJIT AVX2
MethodJobsizeMeanStdDevRatio
CopyToPR512438.7 μs106.58 μs0.75
CopyTomain512588.0 μs15.62 μs1.00
CopyToOverwritePR512298.8 μs6.18 μs0.79
CopyToOverwritemain512381.4 μs4.55 μs1.00
CopyToPR4096237.0 μs6.29 μs0.39
CopyTomain4096606.9 μs37.95 μs1.00
CopyToOverwritePR4096293.7 μs7.59 μs0.76
CopyToOverwritemain4096388.1 μs3.02 μs1.00
CopyToPR1048576239.9 μs10.95 μs0.09
CopyTomain10485762,567.9 μs107.61 μs1.00
CopyToOverwritePR1048576298.2 μs11.47 μs0.12
CopyToOverwritemain10485762,585.2 μs63.77 μs1.00
CopyToPR104857600247.0 μs8.00 μs0.001
CopyTomain104857600230,797.9 μs42,923.74 μs1.000
CopyToOverwritePR104857600291.4 μs6.45 μs0.001
CopyToOverwritemain104857600256,931.8 μs2,784.23 μs1.000

Great job @hamarb123 !!

@stephentoub

stephentoub commented Jun 13, 2023

Copy link
Copy Markdown
Member

The perf win is very impressive.

While this isn't a common scenario, in the name of completeness, what does the perf look like if, after copying, the benchmark then modifies the destination file in some way?

@adamsitnik

Copy link
Copy Markdown
Member

While this isn't a common scenario, in the name of completeness, what does the perf look like if, after copying, the benchmark then modifies the destination file in some way?

That is a very good question!

I've modified the benchmarks and just added File.WriteAllBytes(dest, File.ReadAllBytes(source)) right after the call to copy:

[Benchmark]
[Arguments(HalfKibibyte)]
[Arguments(FourKibibytes)]
[Arguments(OneMibibyte)]
[Arguments(HundredMibibytes)]
public void CopyTo_AndModify(int size)
{
File.Delete(_testFilePath);
File.Copy(_filesToRead[size], _testFilePath); // overwrite defaults to false
+ File.WriteAllBytes(_testFilePath, File.ReadAllBytes(_filesToRead[size]));
}
[Benchmark]
[Arguments(HalfKibibyte)]
[Arguments(FourKibibytes)]
[Arguments(OneMibibyte)]
[Arguments(HundredMibibytes)]
public void CopyToOverwrite_AndModify(int size)
{
File.Copy(_filesToRead[size], _testFilePath, overwrite: true);
+ File.WriteAllBytes(_testFilePath, File.ReadAllBytes(_filesToRead[size]));
}

We have still a major win for large files and no regression for smaller files

MethodJobsizeMeanStdDevRatio
CopyTo_AndModifyPR512533.1 μs16.83 μs0.71
CopyTo_AndModifymain512742.4 μs6.98 μs1.00
CopyToOverwrite_AndModifyPR512588.8 μs25.28 μs0.98
CopyToOverwrite_AndModifymain512593.9 μs10.35 μs1.00
CopyTo_AndModifyPR4096540.8 μs15.53 μs0.71
CopyTo_AndModifymain4096767.0 μs13.17 μs1.00
CopyToOverwrite_AndModifyPR4096590.7 μs19.49 μs1.00
CopyToOverwrite_AndModifymain4096594.7 μs5.73 μs1.00
CopyTo_AndModifyPR10485762,383.3 μs84.83 μs0.64
CopyTo_AndModifymain10485763,701.9 μs82.21 μs1.00
CopyToOverwrite_AndModifyPR10485762,358.2 μs69.96 μs0.69
CopyToOverwrite_AndModifymain10485763,412.3 μs87.54 μs1.00
CopyTo_AndModifyPR104857600167,426.8 μs18,209.30 μs0.61
CopyTo_AndModifymain104857600276,862.8 μs29,743.99 μs1.00
CopyToOverwrite_AndModifyPR104857600170,189.9 μs26,538.74 μs0.61
CopyToOverwrite_AndModifymain104857600287,618.2 μs48,952.24 μs1.00

@stephentoub

Copy link
Copy Markdown
Member

File.WriteAllBytes

WriteAllBytes opens the file as Create, which means it's going to effectively delete the destination and write a new file rather than modify the one that's there.

@adamsitnik

Copy link
Copy Markdown
Member

WriteAllBytes opens the file as Create, which means it's going to effectively delete the destination and write a new file rather than modify the one that's there.

Sorry, I forgot about that (BTW it just proves that we should really add #84532)

Updated benchmark:

privatebyte[]_oneKb=newbyte[1024];[Benchmark][Arguments(HalfKibibyte)][Arguments(FourKibibytes)][Arguments(OneMibibyte)][Arguments(HundredMibibytes)]publicvoidCopyTo_AndModify(intsize){File.Delete(_testFilePath);File.Copy(_filesToRead[size],_testFilePath);// overwrite defaults to falseusingFileStreamfs=File.OpenWrite(_testFilePath);fs.Write(_oneKb);}[Benchmark][Arguments(HalfKibibyte)][Arguments(FourKibibytes)][Arguments(OneMibibyte)][Arguments(HundredMibibytes)]publicvoidCopyToOverwrite_AndModify(intsize){File.Copy(_filesToRead[size],_testFilePath,overwrite:true);usingFileStreamfs=File.OpenWrite(_testFilePath);fs.Write(_oneKb);}

And the results:

BenchmarkDotNet=v0.13.2.2052-nightly, OS=macOS Monterey 12.6.5 (21G531) [Darwin 21.6.0]
Intel Core i7-5557U CPU 3.10GHz (Broadwell), 1 CPU, 4 logical and 2 physical cores
.NET SDK=8.0.100-preview.6.23312.5
[Host] : .NET 8.0.0 (8.0.23.30704), X64 RyuJIT AVX2
PR : .NET 8.0.0 (42.42.42.42424), X64 RyuJIT AVX2
main : .NET 8.0.0 (8.0.23.30704), X64 RyuJIT AVX2
IterationCount=40
MethodJobsizeMeanStdDevRatio
CopyTo_AndModifyPR512537.7 μs13.18 μs0.82
CopyTo_AndModifymain512661.3 μs9.98 μs1.00
CopyToOverwrite_AndModifyPR512549.1 μs16.54 μs1.15
CopyToOverwrite_AndModifymain512476.3 μs7.58 μs1.00
CopyTo_AndModifyPR4096669.8 μs20.57 μs1.01
CopyTo_AndModifymain4096662.4 μs11.60 μs1.00
CopyToOverwrite_AndModifyPR4096695.3 μs26.41 μs1.50
CopyToOverwrite_AndModifymain4096465.0 μs4.94 μs1.00
CopyTo_AndModifyPR1048576722.5 μs14.36 μs0.35
CopyTo_AndModifymain10485762,051.8 μs40.83 μs1.00
CopyToOverwrite_AndModifyPR1048576728.4 μs34.28 μs0.37
CopyToOverwrite_AndModifymain10485761,991.1 μs163.53 μs1.00
CopyTo_AndModifyPR104857600804.9 μs18.60 μs0.007
CopyTo_AndModifymain104857600109,415.3 μs8,542.98 μs1.000
CopyToOverwrite_AndModifyPR104857600762.1 μs26.40 μs0.007
CopyToOverwrite_AndModifymain104857600117,399.2 μs16,068.51 μs1.000

@adamsitnik
adamsitnik merged commit 8fb25a8 into dotnet:mainJun 13, 2023
@adamsitnikadamsitnik added this to the 8.0.0 milestone Jun 13, 2023
@stephentoub

Copy link
Copy Markdown
Member

BTW it just proves that we should really add #84532

Not really 😄

Updated benchmark

Thanks. So this does regress small overwrites that are subsequently modified.

@adamsitnik

Copy link
Copy Markdown
Member

Not really

Why? Do you believe that appending a buffer of bytes to an existing file is a rare scenario?

@danmoseley

Copy link
Copy Markdown
Contributor

Thanks for this nice change @hamarb123 . Would you like to tackle another issue? There are also a number marked api-approved that are available.

@stephentoub

stephentoub commented Jun 13, 2023

Copy link
Copy Markdown
Member

BTW it just proves that we should really add #84532

Not really

Why? Do you believe that appending a buffer of bytes to an existing file is a rare scenario?

I'm not commenting on the scenario of appending a buffer of bytes to an existing file. I'm commenting that a need to mutate a recently copied file as part of benchmarking a copy-on-write optimization and to achieve that writing two short lines of code doesn't prove that we need to add a new API.

@hamarb123

Copy link
Copy Markdown
ContributorAuthor

Thanks for this nice change @hamarb123 . Would you like to tackle another issue? There are also a number marked api-approved that are available.

Thanks! I'll have a look tomorrow (also known as a today at a sensible hour lol). I was also going to do the one for ReFS on Windows, was setting up a VM to test dev drives yesterday.

Copying tests in System.IO.Tests.Perf_FileStream did not improve (haven't included them here), do we want to work out why and improve that API also?

Just following up on this also. Presumably this API just does a block copy with the files. Is this something that's commonly used (I know I've never done it like this)? Because it doesn't seem to benefit from this, but maybe no-one uses it, idk.

@adamsitnik

Copy link
Copy Markdown
Member

Copying tests in System.IO.Tests.Perf_FileStream did not improve

IIRC The FileStream.CopyTo(Stream destination) has no special handling for cases where the destination stream is also a file stream.

Is this something that's commonly used

I don't have enough data to answer your question, but if you can provide an optimization that improves such scenario without introducing a lot of code complexity I am happy to review it.

@danmoseley

Copy link
Copy Markdown
Contributor

I was also going to do the one for ReFS on Windows

Ah yes, indeed that would be nice to get into .NET 8

@ghostghost locked as resolved and limited conversation to collaborators Jul 15, 2023
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-System.IOcommunity-contributionIndicates that the PR has been added by a community memberos-mac-os-xmacOS aka OSX

Projects

None yet

Development

Successfully merging this pull request may close these issues.

File.Copy: Clone file when possible on MacOS

10 participants

@hamarb123@adamsitnik@filipnavara@akoeplinger@stephentoub@danmoseley@jkotas@tmds@slang25@marek-safar
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' Clone files on OSX-like platforms when possible, instead of copying the whole file by hamarb123 · Pull Request #79243 · dotnet/runtime · GitHub
Skip to content

Clone files on OSX-like platforms when possible, instead of copying the whole file - #79243

Merged
adamsitnik merged 85 commits into
dotnet:mainfrom
hamarb123:main
Jun 13, 2023
Merged

Clone files on OSX-like platforms when possible, instead of copying the whole file#79243
adamsitnik merged 85 commits into
dotnet:mainfrom
hamarb123:main

Conversation

@hamarb123

@hamarb123hamarb123 commented Dec 5, 2022

Copy link
Copy Markdown
Contributor

Fixes#77835.

This should improve performance greatly for File.Copy when copying from one APFS volume to a different file in the volume (I think it's within a volume anyway). It will clone the file which is instant instead of copying all of the bytes, and if cloning doesn't work it will fall back to the normal method of copying.

Informative:

  • Uses clonefile when possible on macOS to clone the file while still keeping file locking logic intact
  • Split common Unix logic into multiple functions that the macOS implementation uses parts of at different times
  • No shim needed since the ABI will not change. Precedent for doing it this way for macOS-specific functions: Fix setting creation date for OSX-like platforms #49555
  • Applies to all OSX-like platforms (macOS, iOS, tvOS, and Mac Catalyst), not just macOS

Todo:

  • Need to add tests to check the file is actually cloned so we know if it works or not
  • The logic in this commit has not been tested thoroughly (but it has been tested somewhat), so will wait to see the GitHub actions failures before refactoring (if needed)
  • Do we want tests copying between different "drives"? I think these would be valuable.

• Use copyfile (with COPYFILE_CLONE_FORCE) when possible on macOS to clone the file while still keeping file locking logic intact
• Split common Unix logic into multiple functions that the macOS implementation uses parts of at different times
• Add string version of ResolveLinkTarget to save the allocation since part of the code needs it
• Need to add tests to check the file is actually cloned so we know if it works or not
@ghostghost added area-System.IO community-contribution Indicates that the PR has been added by a community member labels Dec 5, 2022
@ghost

ghost commented Dec 5, 2022

Copy link
Copy Markdown

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

Issue Details

Fixes #77835.

This should improve performance greatly for File.Copy when copying from one APFS volume to a different file in the volume (I think it's within a volume anyway). It will clone the file which is instant instead of copying all of the bytes, and if cloning doesn't work it will fall back to the normal method of copying.

Informative:

  • Uses copyfile (with COPYFILE_CLONE_FORCE) when possible on macOS to clone the file while still keeping file locking logic intact
  • Split common Unix logic into multiple functions that the macOS implementation uses parts of at different times
  • No shim needed since the ABI will not change. Precedent for doing it this way for macOS-specific functions: Fix setting creation date for OSX-like platforms #49555
  • Applies to all OSX-like platforms, not just macOS

Todo:

  • Need to add tests to check the file is actually cloned so we know if it works or not
  • The logic in this commit has not been tested thoroughly (but it has been tested somewhat), so will wait to see the GitHub actions issues before refactoring (if needed)
  • Is it a problem that it could theoretically delete the destination file and then fail for some other reason, leaving the destination file deleted when it may not have done so before (realistically this is extremely unlikely)?
Author:hamarb123
Assignees:-
Labels:

area-System.IO

Milestone:-

@hamarb123

Copy link
Copy Markdown
ContributorAuthor

Btw, this implementation is not done, I want to see if any tests fail first though.

• I missed setting StringMarshalling on the LibraryImport
• I missed partial on the CopyFile method implementations
• Apparently 'Both partial method declarations must be unsafe or neither may be unsafe'
• I love partial methods
• Move unsafe keyword into the OSX method rather than in method declaration
• Fix accessibility modifier of CopyFile in FileSystem.Unix.cs
• Fix indentation (should have used spaces)
• Import Microsoft.Win32.SafeHandles in FileSystem.CopyFile.OSX.cs file
• Fix for SA1205 'Partial elements should declare an access modifier'
• Fix typo in FileSystem.CopyFile.OtherUnix.cs of 'startedCopyFile'
• Fix missing openDst parameter code in StartCopyFile
• Fix not checking the nullability in StandardCopyFile, which led to 'Possible null reference argument for parameter'
• Add missing parameter value (openNewFile) to OpenCopyFileDstHandle
• Fix misnamed variable usages throughout some code
• Properly qualify Interop.ErrorInfo
• This should be the last fix for build issues for this set
@hamarb123

Copy link
Copy Markdown
ContributorAuthor

Sorry for all of the fix commits, I couldn't get build locally to work, so I copied the code into a new project to test it and must have missed some of the changes here.

@hamarb123
hamarb123 marked this pull request as draft December 5, 2022 21:44
• Add missing nullable ? for dstHandle (which obtains the file lock)
• Test failures were since copying a file onto itself should cause an error, this fixes that
…ain)
• The variable in the if statements weren't updated
• Handling for if readlink fails
…t#77835'
• The source path should have used sourceFullPath rather than destPath
@marek-safarmarek-safar added the os-mac-os-x macOS aka OSX label Dec 6, 2022
@hamarb123

hamarb123 commented Dec 6, 2022

Copy link
Copy Markdown
ContributorAuthor

@marek-safar, this applies to macOS, iOS, tvOS, and Mac Catalyst in case you wanted to add tags for those. Can you also 'assign' me if you're able? Thanks.

@hamarb123hamarb123 changed the title [WIP] Cloning files on macOS when possible instead of copying the whole file[WIP] Cloning files on OSX-like platforms when possible instead of copying the whole fileDec 6, 2022
@ghostghost closed this Jan 6, 2023
@ghost

ghost commented Jan 6, 2023

Copy link
Copy Markdown

Draft Pull Request was automatically closed for 30 days of inactivity. Please let us know if you'd like to reopen it.

@adamsitnik

Copy link
Copy Markdown
Member

@filipnavara is there any chance you could review this PR? You are currently the most experience person in this particular area.

@hamarb123

Copy link
Copy Markdown
ContributorAuthor

Is there a reason why 24 test runs have been cancelled so far? Do the tests need to be re-run?

@danmoseley

Copy link
Copy Markdown
Contributor

@hamarb123 it is dotnet/arcade#13774

@slang25

Copy link
Copy Markdown
Contributor

I'm really looking forward to this change! Will we be seeing it land in a .NET 8 preview?

@danmoseley

Copy link
Copy Markdown
Contributor

I'm really looking forward to this change! Will we be seeing it land in a .NET 8 preview?

If it's merged in the next month or two, it will be in .NET 8.

@hamarb123

Copy link
Copy Markdown
ContributorAuthor

I'm really looking forward to this change! Will we be seeing it land in a .NET 8 preview?

It's hopefully just a few more stages of review at most. I'm hoping it will make it to .NET 8 also.

@tmds

tmds commented Jun 13, 2023

Copy link
Copy Markdown
Member

Per @adamsitnik's earlier comment, macOS isn't benchmarked in the perf lab.

So before merging, we need some benchmark results that verify there is a performance gain.

@hamarb123 can you look at the documentation Adam shared, and run the benchmarks on your machine?

@hamarb123

hamarb123 commented Jun 13, 2023

Copy link
Copy Markdown
ContributorAuthor

Benchmark results:

  • CopyTo is ~1.9x faster for small files with the changes, and is usually ~180us for all the tests up to 1MB (~207us for 100MB test). Note this test also deletes the file as a part of the test, so simply copying would likely be faster again.
  • CopyToOverwrite is ~1.15x faster for small files with the changes, and is ~215us for all the tests.
  • Copying tests in System.IO.Tests.Perf_FileStream did not improve (haven't included them here), do we want to work out why and improve that API also?
  • The comparison was between the commit just before this comment which merges all of the new runtime changes, and the runtime repo with those same runtime changes but without this PR.

System.IO.Tests.Perf_File

With changes
MethodsizeMeanErrorStdDevMedianMinMaxAllocated
CopyTo512180.4 us3.42 us3.66 us180.0 us173.9 us186.3 us65 B
CopyToOverwrite512217.4 us4.17 us4.10 us217.2 us210.2 us225.9 us129 B
CopyTo4096175.9 us3.41 us3.19 us175.0 us171.2 us182.7 us65 B
CopyToOverwrite4096214.0 us2.41 us2.13 us213.4 us209.8 us218.2 us129 B
CopyTo1048576175.9 us2.41 us2.01 us175.9 us173.4 us179.8 us65 B
CopyToOverwrite1048576212.3 us3.97 us3.31 us212.0 us208.1 us218.1 us129 B
CopyTo104857600206.9 us2.32 us1.94 us206.6 us203.7 us210.8 us65 B
CopyToOverwrite104857600213.9 us3.58 us3.17 us214.2 us209.1 us218.0 us129 B
Without changes
MethodsizeMeanErrorStdDevMedianMinMaxAllocated
CopyTo512347.6 us12.46 us14.35 us344.8 us326.4 us374.2 us129 B
CopyToOverwrite512249.2 us5.56 us6.18 us248.7 us241.4 us261.1 us129 B
CopyTo4096347.1 us7.16 us7.36 us345.2 us335.4 us364.5 us129 B
CopyToOverwrite4096244.6 us3.07 us2.72 us243.9 us240.4 us248.4 us129 B
CopyTo10485761,313.5 us68.13 us72.90 us1,322.9 us1,191.9 us1,437.3 us133 B
CopyToOverwrite10485761,773.5 us321.63 us370.39 us1,730.5 us1,184.8 us2,432.6 us136 B
CopyTo10485760055,961.7 us4,954.14 us5,705.19 us53,346.9 us49,530.1 us65,915.7 us-
CopyToOverwrite10485760055,883.4 us5,472.86 us6,302.55 us54,004.8 us46,797.4 us66,152.4 us-

@tmds

tmds commented Jun 13, 2023

Copy link
Copy Markdown
Member

CopyToOverwrite is ~1.15x faster for small files with the changes

That's a pleasant surprise.

My expectation was to see a regression for small files with overwrite since we're making additional system calls and the benefit of cloning is low for small files.

@adamsitnikadamsitnik left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The code looks very good, I found only one thing that could be improved (PTAL at my comment).

The perf win is very impressive. I've benchmarked it on my old macBook:

BenchmarkDotNet=v0.13.2.2052-nightly, OS=macOS Monterey 12.6.5 (21G531) [Darwin 21.6.0]
Intel Core i7-5557U CPU 3.10GHz (Broadwell), 1 CPU, 4 logical and 2 physical cores
.NET SDK=8.0.100-preview.6.23312.5
[Host] : .NET 8.0.0 (8.0.23.30704), X64 RyuJIT AVX2
PR : .NET 8.0.0 (42.42.42.42424), X64 RyuJIT AVX2
main : .NET 8.0.0 (8.0.23.30704), X64 RyuJIT AVX2
MethodJobsizeMeanStdDevRatio
CopyToPR512438.7 μs106.58 μs0.75
CopyTomain512588.0 μs15.62 μs1.00
CopyToOverwritePR512298.8 μs6.18 μs0.79
CopyToOverwritemain512381.4 μs4.55 μs1.00
CopyToPR4096237.0 μs6.29 μs0.39
CopyTomain4096606.9 μs37.95 μs1.00
CopyToOverwritePR4096293.7 μs7.59 μs0.76
CopyToOverwritemain4096388.1 μs3.02 μs1.00
CopyToPR1048576239.9 μs10.95 μs0.09
CopyTomain10485762,567.9 μs107.61 μs1.00
CopyToOverwritePR1048576298.2 μs11.47 μs0.12
CopyToOverwritemain10485762,585.2 μs63.77 μs1.00
CopyToPR104857600247.0 μs8.00 μs0.001
CopyTomain104857600230,797.9 μs42,923.74 μs1.000
CopyToOverwritePR104857600291.4 μs6.45 μs0.001
CopyToOverwritemain104857600256,931.8 μs2,784.23 μs1.000

Great job @hamarb123 !!

@stephentoub

stephentoub commented Jun 13, 2023

Copy link
Copy Markdown
Member

The perf win is very impressive.

While this isn't a common scenario, in the name of completeness, what does the perf look like if, after copying, the benchmark then modifies the destination file in some way?

@adamsitnik

Copy link
Copy Markdown
Member

While this isn't a common scenario, in the name of completeness, what does the perf look like if, after copying, the benchmark then modifies the destination file in some way?

That is a very good question!

I've modified the benchmarks and just added File.WriteAllBytes(dest, File.ReadAllBytes(source)) right after the call to copy:

[Benchmark]
[Arguments(HalfKibibyte)]
[Arguments(FourKibibytes)]
[Arguments(OneMibibyte)]
[Arguments(HundredMibibytes)]
public void CopyTo_AndModify(int size)
{
File.Delete(_testFilePath);
File.Copy(_filesToRead[size], _testFilePath); // overwrite defaults to false
+ File.WriteAllBytes(_testFilePath, File.ReadAllBytes(_filesToRead[size]));
}
[Benchmark]
[Arguments(HalfKibibyte)]
[Arguments(FourKibibytes)]
[Arguments(OneMibibyte)]
[Arguments(HundredMibibytes)]
public void CopyToOverwrite_AndModify(int size)
{
File.Copy(_filesToRead[size], _testFilePath, overwrite: true);
+ File.WriteAllBytes(_testFilePath, File.ReadAllBytes(_filesToRead[size]));
}

We have still a major win for large files and no regression for smaller files

MethodJobsizeMeanStdDevRatio
CopyTo_AndModifyPR512533.1 μs16.83 μs0.71
CopyTo_AndModifymain512742.4 μs6.98 μs1.00
CopyToOverwrite_AndModifyPR512588.8 μs25.28 μs0.98
CopyToOverwrite_AndModifymain512593.9 μs10.35 μs1.00
CopyTo_AndModifyPR4096540.8 μs15.53 μs0.71
CopyTo_AndModifymain4096767.0 μs13.17 μs1.00
CopyToOverwrite_AndModifyPR4096590.7 μs19.49 μs1.00
CopyToOverwrite_AndModifymain4096594.7 μs5.73 μs1.00
CopyTo_AndModifyPR10485762,383.3 μs84.83 μs0.64
CopyTo_AndModifymain10485763,701.9 μs82.21 μs1.00
CopyToOverwrite_AndModifyPR10485762,358.2 μs69.96 μs0.69
CopyToOverwrite_AndModifymain10485763,412.3 μs87.54 μs1.00
CopyTo_AndModifyPR104857600167,426.8 μs18,209.30 μs0.61
CopyTo_AndModifymain104857600276,862.8 μs29,743.99 μs1.00
CopyToOverwrite_AndModifyPR104857600170,189.9 μs26,538.74 μs0.61
CopyToOverwrite_AndModifymain104857600287,618.2 μs48,952.24 μs1.00

@stephentoub

Copy link
Copy Markdown
Member

File.WriteAllBytes

WriteAllBytes opens the file as Create, which means it's going to effectively delete the destination and write a new file rather than modify the one that's there.

@adamsitnik

Copy link
Copy Markdown
Member

WriteAllBytes opens the file as Create, which means it's going to effectively delete the destination and write a new file rather than modify the one that's there.

Sorry, I forgot about that (BTW it just proves that we should really add #84532)

Updated benchmark:

privatebyte[]_oneKb=newbyte[1024];[Benchmark][Arguments(HalfKibibyte)][Arguments(FourKibibytes)][Arguments(OneMibibyte)][Arguments(HundredMibibytes)]publicvoidCopyTo_AndModify(intsize){File.Delete(_testFilePath);File.Copy(_filesToRead[size],_testFilePath);// overwrite defaults to falseusingFileStreamfs=File.OpenWrite(_testFilePath);fs.Write(_oneKb);}[Benchmark][Arguments(HalfKibibyte)][Arguments(FourKibibytes)][Arguments(OneMibibyte)][Arguments(HundredMibibytes)]publicvoidCopyToOverwrite_AndModify(intsize){File.Copy(_filesToRead[size],_testFilePath,overwrite:true);usingFileStreamfs=File.OpenWrite(_testFilePath);fs.Write(_oneKb);}

And the results:

BenchmarkDotNet=v0.13.2.2052-nightly, OS=macOS Monterey 12.6.5 (21G531) [Darwin 21.6.0]
Intel Core i7-5557U CPU 3.10GHz (Broadwell), 1 CPU, 4 logical and 2 physical cores
.NET SDK=8.0.100-preview.6.23312.5
[Host] : .NET 8.0.0 (8.0.23.30704), X64 RyuJIT AVX2
PR : .NET 8.0.0 (42.42.42.42424), X64 RyuJIT AVX2
main : .NET 8.0.0 (8.0.23.30704), X64 RyuJIT AVX2
IterationCount=40
MethodJobsizeMeanStdDevRatio
CopyTo_AndModifyPR512537.7 μs13.18 μs0.82
CopyTo_AndModifymain512661.3 μs9.98 μs1.00
CopyToOverwrite_AndModifyPR512549.1 μs16.54 μs1.15
CopyToOverwrite_AndModifymain512476.3 μs7.58 μs1.00
CopyTo_AndModifyPR4096669.8 μs20.57 μs1.01
CopyTo_AndModifymain4096662.4 μs11.60 μs1.00
CopyToOverwrite_AndModifyPR4096695.3 μs26.41 μs1.50
CopyToOverwrite_AndModifymain4096465.0 μs4.94 μs1.00
CopyTo_AndModifyPR1048576722.5 μs14.36 μs0.35
CopyTo_AndModifymain10485762,051.8 μs40.83 μs1.00
CopyToOverwrite_AndModifyPR1048576728.4 μs34.28 μs0.37
CopyToOverwrite_AndModifymain10485761,991.1 μs163.53 μs1.00
CopyTo_AndModifyPR104857600804.9 μs18.60 μs0.007
CopyTo_AndModifymain104857600109,415.3 μs8,542.98 μs1.000
CopyToOverwrite_AndModifyPR104857600762.1 μs26.40 μs0.007
CopyToOverwrite_AndModifymain104857600117,399.2 μs16,068.51 μs1.000

@adamsitnik
adamsitnik merged commit 8fb25a8 into dotnet:mainJun 13, 2023
@adamsitnikadamsitnik added this to the 8.0.0 milestone Jun 13, 2023
@stephentoub

Copy link
Copy Markdown
Member

BTW it just proves that we should really add #84532

Not really 😄

Updated benchmark

Thanks. So this does regress small overwrites that are subsequently modified.

@adamsitnik

Copy link
Copy Markdown
Member

Not really

Why? Do you believe that appending a buffer of bytes to an existing file is a rare scenario?

@danmoseley

Copy link
Copy Markdown
Contributor

Thanks for this nice change @hamarb123 . Would you like to tackle another issue? There are also a number marked api-approved that are available.

@stephentoub

stephentoub commented Jun 13, 2023

Copy link
Copy Markdown
Member

BTW it just proves that we should really add #84532

Not really

Why? Do you believe that appending a buffer of bytes to an existing file is a rare scenario?

I'm not commenting on the scenario of appending a buffer of bytes to an existing file. I'm commenting that a need to mutate a recently copied file as part of benchmarking a copy-on-write optimization and to achieve that writing two short lines of code doesn't prove that we need to add a new API.

@hamarb123

Copy link
Copy Markdown
ContributorAuthor

Thanks for this nice change @hamarb123 . Would you like to tackle another issue? There are also a number marked api-approved that are available.

Thanks! I'll have a look tomorrow (also known as a today at a sensible hour lol). I was also going to do the one for ReFS on Windows, was setting up a VM to test dev drives yesterday.

Copying tests in System.IO.Tests.Perf_FileStream did not improve (haven't included them here), do we want to work out why and improve that API also?

Just following up on this also. Presumably this API just does a block copy with the files. Is this something that's commonly used (I know I've never done it like this)? Because it doesn't seem to benefit from this, but maybe no-one uses it, idk.

@adamsitnik

Copy link
Copy Markdown
Member

Copying tests in System.IO.Tests.Perf_FileStream did not improve

IIRC The FileStream.CopyTo(Stream destination) has no special handling for cases where the destination stream is also a file stream.

Is this something that's commonly used

I don't have enough data to answer your question, but if you can provide an optimization that improves such scenario without introducing a lot of code complexity I am happy to review it.

@danmoseley

Copy link
Copy Markdown
Contributor

I was also going to do the one for ReFS on Windows

Ah yes, indeed that would be nice to get into .NET 8

@ghostghost locked as resolved and limited conversation to collaborators Jul 15, 2023
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-System.IOcommunity-contributionIndicates that the PR has been added by a community memberos-mac-os-xmacOS aka OSX

Projects

None yet

Development

Successfully merging this pull request may close these issues.

File.Copy: Clone file when possible on MacOS

10 participants

@hamarb123@adamsitnik@filipnavara@akoeplinger@stephentoub@danmoseley@jkotas@tmds@slang25@marek-safar
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Clone files on OSX-like platforms when possible, instead of copying the whole file by hamarb123 · Pull Request #79243 · dotnet/runtime · GitHub
Skip to content

Clone files on OSX-like platforms when possible, instead of copying the whole file - #79243

Merged
adamsitnik merged 85 commits into
dotnet:mainfrom
hamarb123:main
Jun 13, 2023
Merged

Clone files on OSX-like platforms when possible, instead of copying the whole file#79243
adamsitnik merged 85 commits into
dotnet:mainfrom
hamarb123:main

Conversation

@hamarb123

@hamarb123hamarb123 commented Dec 5, 2022

Copy link
Copy Markdown
Contributor

Fixes#77835.

This should improve performance greatly for File.Copy when copying from one APFS volume to a different file in the volume (I think it's within a volume anyway). It will clone the file which is instant instead of copying all of the bytes, and if cloning doesn't work it will fall back to the normal method of copying.

Informative:

  • Uses clonefile when possible on macOS to clone the file while still keeping file locking logic intact
  • Split common Unix logic into multiple functions that the macOS implementation uses parts of at different times
  • No shim needed since the ABI will not change. Precedent for doing it this way for macOS-specific functions: Fix setting creation date for OSX-like platforms #49555
  • Applies to all OSX-like platforms (macOS, iOS, tvOS, and Mac Catalyst), not just macOS

Todo:

  • Need to add tests to check the file is actually cloned so we know if it works or not
  • The logic in this commit has not been tested thoroughly (but it has been tested somewhat), so will wait to see the GitHub actions failures before refactoring (if needed)
  • Do we want tests copying between different "drives"? I think these would be valuable.

• Use copyfile (with COPYFILE_CLONE_FORCE) when possible on macOS to clone the file while still keeping file locking logic intact
• Split common Unix logic into multiple functions that the macOS implementation uses parts of at different times
• Add string version of ResolveLinkTarget to save the allocation since part of the code needs it
• Need to add tests to check the file is actually cloned so we know if it works or not
@ghostghost added area-System.IO community-contribution Indicates that the PR has been added by a community member labels Dec 5, 2022
@ghost

ghost commented Dec 5, 2022

Copy link
Copy Markdown

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

Issue Details

Fixes #77835.

This should improve performance greatly for File.Copy when copying from one APFS volume to a different file in the volume (I think it's within a volume anyway). It will clone the file which is instant instead of copying all of the bytes, and if cloning doesn't work it will fall back to the normal method of copying.

Informative:

  • Uses copyfile (with COPYFILE_CLONE_FORCE) when possible on macOS to clone the file while still keeping file locking logic intact
  • Split common Unix logic into multiple functions that the macOS implementation uses parts of at different times
  • No shim needed since the ABI will not change. Precedent for doing it this way for macOS-specific functions: Fix setting creation date for OSX-like platforms #49555
  • Applies to all OSX-like platforms, not just macOS

Todo:

  • Need to add tests to check the file is actually cloned so we know if it works or not
  • The logic in this commit has not been tested thoroughly (but it has been tested somewhat), so will wait to see the GitHub actions issues before refactoring (if needed)
  • Is it a problem that it could theoretically delete the destination file and then fail for some other reason, leaving the destination file deleted when it may not have done so before (realistically this is extremely unlikely)?
Author:hamarb123
Assignees:-
Labels:

area-System.IO

Milestone:-

@hamarb123

Copy link
Copy Markdown
ContributorAuthor

Btw, this implementation is not done, I want to see if any tests fail first though.

• I missed setting StringMarshalling on the LibraryImport
• I missed partial on the CopyFile method implementations
• Apparently 'Both partial method declarations must be unsafe or neither may be unsafe'
• I love partial methods
• Move unsafe keyword into the OSX method rather than in method declaration
• Fix accessibility modifier of CopyFile in FileSystem.Unix.cs
• Fix indentation (should have used spaces)
• Import Microsoft.Win32.SafeHandles in FileSystem.CopyFile.OSX.cs file
• Fix for SA1205 'Partial elements should declare an access modifier'
• Fix typo in FileSystem.CopyFile.OtherUnix.cs of 'startedCopyFile'
• Fix missing openDst parameter code in StartCopyFile
• Fix not checking the nullability in StandardCopyFile, which led to 'Possible null reference argument for parameter'
• Add missing parameter value (openNewFile) to OpenCopyFileDstHandle
• Fix misnamed variable usages throughout some code
• Properly qualify Interop.ErrorInfo
• This should be the last fix for build issues for this set
@hamarb123

Copy link
Copy Markdown
ContributorAuthor

Sorry for all of the fix commits, I couldn't get build locally to work, so I copied the code into a new project to test it and must have missed some of the changes here.

@hamarb123
hamarb123 marked this pull request as draft December 5, 2022 21:44
• Add missing nullable ? for dstHandle (which obtains the file lock)
• Test failures were since copying a file onto itself should cause an error, this fixes that
…ain)
• The variable in the if statements weren't updated
• Handling for if readlink fails
…t#77835'
• The source path should have used sourceFullPath rather than destPath
@marek-safarmarek-safar added the os-mac-os-x macOS aka OSX label Dec 6, 2022
@hamarb123

hamarb123 commented Dec 6, 2022

Copy link
Copy Markdown
ContributorAuthor

@marek-safar, this applies to macOS, iOS, tvOS, and Mac Catalyst in case you wanted to add tags for those. Can you also 'assign' me if you're able? Thanks.

@hamarb123hamarb123 changed the title [WIP] Cloning files on macOS when possible instead of copying the whole file[WIP] Cloning files on OSX-like platforms when possible instead of copying the whole fileDec 6, 2022
@ghostghost closed this Jan 6, 2023
@ghost

ghost commented Jan 6, 2023

Copy link
Copy Markdown

Draft Pull Request was automatically closed for 30 days of inactivity. Please let us know if you'd like to reopen it.

@adamsitnik

Copy link
Copy Markdown
Member

@filipnavara is there any chance you could review this PR? You are currently the most experience person in this particular area.

@hamarb123

Copy link
Copy Markdown
ContributorAuthor

Is there a reason why 24 test runs have been cancelled so far? Do the tests need to be re-run?

@danmoseley

Copy link
Copy Markdown
Contributor

@hamarb123 it is dotnet/arcade#13774

@slang25

Copy link
Copy Markdown
Contributor

I'm really looking forward to this change! Will we be seeing it land in a .NET 8 preview?

@danmoseley

Copy link
Copy Markdown
Contributor

I'm really looking forward to this change! Will we be seeing it land in a .NET 8 preview?

If it's merged in the next month or two, it will be in .NET 8.

@hamarb123

Copy link
Copy Markdown
ContributorAuthor

I'm really looking forward to this change! Will we be seeing it land in a .NET 8 preview?

It's hopefully just a few more stages of review at most. I'm hoping it will make it to .NET 8 also.

@tmds

tmds commented Jun 13, 2023

Copy link
Copy Markdown
Member

Per @adamsitnik's earlier comment, macOS isn't benchmarked in the perf lab.

So before merging, we need some benchmark results that verify there is a performance gain.

@hamarb123 can you look at the documentation Adam shared, and run the benchmarks on your machine?

@hamarb123

hamarb123 commented Jun 13, 2023

Copy link
Copy Markdown
ContributorAuthor

Benchmark results:

  • CopyTo is ~1.9x faster for small files with the changes, and is usually ~180us for all the tests up to 1MB (~207us for 100MB test). Note this test also deletes the file as a part of the test, so simply copying would likely be faster again.
  • CopyToOverwrite is ~1.15x faster for small files with the changes, and is ~215us for all the tests.
  • Copying tests in System.IO.Tests.Perf_FileStream did not improve (haven't included them here), do we want to work out why and improve that API also?
  • The comparison was between the commit just before this comment which merges all of the new runtime changes, and the runtime repo with those same runtime changes but without this PR.

System.IO.Tests.Perf_File

With changes
MethodsizeMeanErrorStdDevMedianMinMaxAllocated
CopyTo512180.4 us3.42 us3.66 us180.0 us173.9 us186.3 us65 B
CopyToOverwrite512217.4 us4.17 us4.10 us217.2 us210.2 us225.9 us129 B
CopyTo4096175.9 us3.41 us3.19 us175.0 us171.2 us182.7 us65 B
CopyToOverwrite4096214.0 us2.41 us2.13 us213.4 us209.8 us218.2 us129 B
CopyTo1048576175.9 us2.41 us2.01 us175.9 us173.4 us179.8 us65 B
CopyToOverwrite1048576212.3 us3.97 us3.31 us212.0 us208.1 us218.1 us129 B
CopyTo104857600206.9 us2.32 us1.94 us206.6 us203.7 us210.8 us65 B
CopyToOverwrite104857600213.9 us3.58 us3.17 us214.2 us209.1 us218.0 us129 B
Without changes
MethodsizeMeanErrorStdDevMedianMinMaxAllocated
CopyTo512347.6 us12.46 us14.35 us344.8 us326.4 us374.2 us129 B
CopyToOverwrite512249.2 us5.56 us6.18 us248.7 us241.4 us261.1 us129 B
CopyTo4096347.1 us7.16 us7.36 us345.2 us335.4 us364.5 us129 B
CopyToOverwrite4096244.6 us3.07 us2.72 us243.9 us240.4 us248.4 us129 B
CopyTo10485761,313.5 us68.13 us72.90 us1,322.9 us1,191.9 us1,437.3 us133 B
CopyToOverwrite10485761,773.5 us321.63 us370.39 us1,730.5 us1,184.8 us2,432.6 us136 B
CopyTo10485760055,961.7 us4,954.14 us5,705.19 us53,346.9 us49,530.1 us65,915.7 us-
CopyToOverwrite10485760055,883.4 us5,472.86 us6,302.55 us54,004.8 us46,797.4 us66,152.4 us-

@tmds

tmds commented Jun 13, 2023

Copy link
Copy Markdown
Member

CopyToOverwrite is ~1.15x faster for small files with the changes

That's a pleasant surprise.

My expectation was to see a regression for small files with overwrite since we're making additional system calls and the benefit of cloning is low for small files.

@adamsitnikadamsitnik left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The code looks very good, I found only one thing that could be improved (PTAL at my comment).

The perf win is very impressive. I've benchmarked it on my old macBook:

BenchmarkDotNet=v0.13.2.2052-nightly, OS=macOS Monterey 12.6.5 (21G531) [Darwin 21.6.0]
Intel Core i7-5557U CPU 3.10GHz (Broadwell), 1 CPU, 4 logical and 2 physical cores
.NET SDK=8.0.100-preview.6.23312.5
[Host] : .NET 8.0.0 (8.0.23.30704), X64 RyuJIT AVX2
PR : .NET 8.0.0 (42.42.42.42424), X64 RyuJIT AVX2
main : .NET 8.0.0 (8.0.23.30704), X64 RyuJIT AVX2
MethodJobsizeMeanStdDevRatio
CopyToPR512438.7 μs106.58 μs0.75
CopyTomain512588.0 μs15.62 μs1.00
CopyToOverwritePR512298.8 μs6.18 μs0.79
CopyToOverwritemain512381.4 μs4.55 μs1.00
CopyToPR4096237.0 μs6.29 μs0.39
CopyTomain4096606.9 μs37.95 μs1.00
CopyToOverwritePR4096293.7 μs7.59 μs0.76
CopyToOverwritemain4096388.1 μs3.02 μs1.00
CopyToPR1048576239.9 μs10.95 μs0.09
CopyTomain10485762,567.9 μs107.61 μs1.00
CopyToOverwritePR1048576298.2 μs11.47 μs0.12
CopyToOverwritemain10485762,585.2 μs63.77 μs1.00
CopyToPR104857600247.0 μs8.00 μs0.001
CopyTomain104857600230,797.9 μs42,923.74 μs1.000
CopyToOverwritePR104857600291.4 μs6.45 μs0.001
CopyToOverwritemain104857600256,931.8 μs2,784.23 μs1.000

Great job @hamarb123 !!

@stephentoub

stephentoub commented Jun 13, 2023

Copy link
Copy Markdown
Member

The perf win is very impressive.

While this isn't a common scenario, in the name of completeness, what does the perf look like if, after copying, the benchmark then modifies the destination file in some way?

@adamsitnik

Copy link
Copy Markdown
Member

While this isn't a common scenario, in the name of completeness, what does the perf look like if, after copying, the benchmark then modifies the destination file in some way?

That is a very good question!

I've modified the benchmarks and just added File.WriteAllBytes(dest, File.ReadAllBytes(source)) right after the call to copy:

[Benchmark]
[Arguments(HalfKibibyte)]
[Arguments(FourKibibytes)]
[Arguments(OneMibibyte)]
[Arguments(HundredMibibytes)]
public void CopyTo_AndModify(int size)
{
File.Delete(_testFilePath);
File.Copy(_filesToRead[size], _testFilePath); // overwrite defaults to false
+ File.WriteAllBytes(_testFilePath, File.ReadAllBytes(_filesToRead[size]));
}
[Benchmark]
[Arguments(HalfKibibyte)]
[Arguments(FourKibibytes)]
[Arguments(OneMibibyte)]
[Arguments(HundredMibibytes)]
public void CopyToOverwrite_AndModify(int size)
{
File.Copy(_filesToRead[size], _testFilePath, overwrite: true);
+ File.WriteAllBytes(_testFilePath, File.ReadAllBytes(_filesToRead[size]));
}

We have still a major win for large files and no regression for smaller files

MethodJobsizeMeanStdDevRatio
CopyTo_AndModifyPR512533.1 μs16.83 μs0.71
CopyTo_AndModifymain512742.4 μs6.98 μs1.00
CopyToOverwrite_AndModifyPR512588.8 μs25.28 μs0.98
CopyToOverwrite_AndModifymain512593.9 μs10.35 μs1.00
CopyTo_AndModifyPR4096540.8 μs15.53 μs0.71
CopyTo_AndModifymain4096767.0 μs13.17 μs1.00
CopyToOverwrite_AndModifyPR4096590.7 μs19.49 μs1.00
CopyToOverwrite_AndModifymain4096594.7 μs5.73 μs1.00
CopyTo_AndModifyPR10485762,383.3 μs84.83 μs0.64
CopyTo_AndModifymain10485763,701.9 μs82.21 μs1.00
CopyToOverwrite_AndModifyPR10485762,358.2 μs69.96 μs0.69
CopyToOverwrite_AndModifymain10485763,412.3 μs87.54 μs1.00
CopyTo_AndModifyPR104857600167,426.8 μs18,209.30 μs0.61
CopyTo_AndModifymain104857600276,862.8 μs29,743.99 μs1.00
CopyToOverwrite_AndModifyPR104857600170,189.9 μs26,538.74 μs0.61
CopyToOverwrite_AndModifymain104857600287,618.2 μs48,952.24 μs1.00

@stephentoub

Copy link
Copy Markdown
Member

File.WriteAllBytes

WriteAllBytes opens the file as Create, which means it's going to effectively delete the destination and write a new file rather than modify the one that's there.

@adamsitnik

Copy link
Copy Markdown
Member

WriteAllBytes opens the file as Create, which means it's going to effectively delete the destination and write a new file rather than modify the one that's there.

Sorry, I forgot about that (BTW it just proves that we should really add #84532)

Updated benchmark:

privatebyte[]_oneKb=newbyte[1024];[Benchmark][Arguments(HalfKibibyte)][Arguments(FourKibibytes)][Arguments(OneMibibyte)][Arguments(HundredMibibytes)]publicvoidCopyTo_AndModify(intsize){File.Delete(_testFilePath);File.Copy(_filesToRead[size],_testFilePath);// overwrite defaults to falseusingFileStreamfs=File.OpenWrite(_testFilePath);fs.Write(_oneKb);}[Benchmark][Arguments(HalfKibibyte)][Arguments(FourKibibytes)][Arguments(OneMibibyte)][Arguments(HundredMibibytes)]publicvoidCopyToOverwrite_AndModify(intsize){File.Copy(_filesToRead[size],_testFilePath,overwrite:true);usingFileStreamfs=File.OpenWrite(_testFilePath);fs.Write(_oneKb);}

And the results:

BenchmarkDotNet=v0.13.2.2052-nightly, OS=macOS Monterey 12.6.5 (21G531) [Darwin 21.6.0]
Intel Core i7-5557U CPU 3.10GHz (Broadwell), 1 CPU, 4 logical and 2 physical cores
.NET SDK=8.0.100-preview.6.23312.5
[Host] : .NET 8.0.0 (8.0.23.30704), X64 RyuJIT AVX2
PR : .NET 8.0.0 (42.42.42.42424), X64 RyuJIT AVX2
main : .NET 8.0.0 (8.0.23.30704), X64 RyuJIT AVX2
IterationCount=40
MethodJobsizeMeanStdDevRatio
CopyTo_AndModifyPR512537.7 μs13.18 μs0.82
CopyTo_AndModifymain512661.3 μs9.98 μs1.00
CopyToOverwrite_AndModifyPR512549.1 μs16.54 μs1.15
CopyToOverwrite_AndModifymain512476.3 μs7.58 μs1.00
CopyTo_AndModifyPR4096669.8 μs20.57 μs1.01
CopyTo_AndModifymain4096662.4 μs11.60 μs1.00
CopyToOverwrite_AndModifyPR4096695.3 μs26.41 μs1.50
CopyToOverwrite_AndModifymain4096465.0 μs4.94 μs1.00
CopyTo_AndModifyPR1048576722.5 μs14.36 μs0.35
CopyTo_AndModifymain10485762,051.8 μs40.83 μs1.00
CopyToOverwrite_AndModifyPR1048576728.4 μs34.28 μs0.37
CopyToOverwrite_AndModifymain10485761,991.1 μs163.53 μs1.00
CopyTo_AndModifyPR104857600804.9 μs18.60 μs0.007
CopyTo_AndModifymain104857600109,415.3 μs8,542.98 μs1.000
CopyToOverwrite_AndModifyPR104857600762.1 μs26.40 μs0.007
CopyToOverwrite_AndModifymain104857600117,399.2 μs16,068.51 μs1.000

@adamsitnik
adamsitnik merged commit 8fb25a8 into dotnet:mainJun 13, 2023
@adamsitnikadamsitnik added this to the 8.0.0 milestone Jun 13, 2023
@stephentoub

Copy link
Copy Markdown
Member

BTW it just proves that we should really add #84532

Not really 😄

Updated benchmark

Thanks. So this does regress small overwrites that are subsequently modified.

@adamsitnik

Copy link
Copy Markdown
Member

Not really

Why? Do you believe that appending a buffer of bytes to an existing file is a rare scenario?

@danmoseley

Copy link
Copy Markdown
Contributor

Thanks for this nice change @hamarb123 . Would you like to tackle another issue? There are also a number marked api-approved that are available.

@stephentoub

stephentoub commented Jun 13, 2023

Copy link
Copy Markdown
Member

BTW it just proves that we should really add #84532

Not really

Why? Do you believe that appending a buffer of bytes to an existing file is a rare scenario?

I'm not commenting on the scenario of appending a buffer of bytes to an existing file. I'm commenting that a need to mutate a recently copied file as part of benchmarking a copy-on-write optimization and to achieve that writing two short lines of code doesn't prove that we need to add a new API.

@hamarb123

Copy link
Copy Markdown
ContributorAuthor

Thanks for this nice change @hamarb123 . Would you like to tackle another issue? There are also a number marked api-approved that are available.

Thanks! I'll have a look tomorrow (also known as a today at a sensible hour lol). I was also going to do the one for ReFS on Windows, was setting up a VM to test dev drives yesterday.

Copying tests in System.IO.Tests.Perf_FileStream did not improve (haven't included them here), do we want to work out why and improve that API also?

Just following up on this also. Presumably this API just does a block copy with the files. Is this something that's commonly used (I know I've never done it like this)? Because it doesn't seem to benefit from this, but maybe no-one uses it, idk.

@adamsitnik

Copy link
Copy Markdown
Member

Copying tests in System.IO.Tests.Perf_FileStream did not improve

IIRC The FileStream.CopyTo(Stream destination) has no special handling for cases where the destination stream is also a file stream.

Is this something that's commonly used

I don't have enough data to answer your question, but if you can provide an optimization that improves such scenario without introducing a lot of code complexity I am happy to review it.

@danmoseley

Copy link
Copy Markdown
Contributor

I was also going to do the one for ReFS on Windows

Ah yes, indeed that would be nice to get into .NET 8

@ghostghost locked as resolved and limited conversation to collaborators Jul 15, 2023
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-System.IOcommunity-contributionIndicates that the PR has been added by a community memberos-mac-os-xmacOS aka OSX

Projects

None yet

Development

Successfully merging this pull request may close these issues.

File.Copy: Clone file when possible on MacOS

10 participants

@hamarb123@adamsitnik@filipnavara@akoeplinger@stephentoub@danmoseley@jkotas@tmds@slang25@marek-safar
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Clone files on OSX-like platforms when possible, instead of copying the whole file by hamarb123 · Pull Request #79243 · dotnet/runtime · GitHub
Skip to content

Clone files on OSX-like platforms when possible, instead of copying the whole file - #79243

Merged
adamsitnik merged 85 commits into
dotnet:mainfrom
hamarb123:main
Jun 13, 2023
Merged

Clone files on OSX-like platforms when possible, instead of copying the whole file#79243
adamsitnik merged 85 commits into
dotnet:mainfrom
hamarb123:main

Conversation

@hamarb123

@hamarb123hamarb123 commented Dec 5, 2022

Copy link
Copy Markdown
Contributor

Fixes#77835.

This should improve performance greatly for File.Copy when copying from one APFS volume to a different file in the volume (I think it's within a volume anyway). It will clone the file which is instant instead of copying all of the bytes, and if cloning doesn't work it will fall back to the normal method of copying.

Informative:

  • Uses clonefile when possible on macOS to clone the file while still keeping file locking logic intact
  • Split common Unix logic into multiple functions that the macOS implementation uses parts of at different times
  • No shim needed since the ABI will not change. Precedent for doing it this way for macOS-specific functions: Fix setting creation date for OSX-like platforms #49555
  • Applies to all OSX-like platforms (macOS, iOS, tvOS, and Mac Catalyst), not just macOS

Todo:

  • Need to add tests to check the file is actually cloned so we know if it works or not
  • The logic in this commit has not been tested thoroughly (but it has been tested somewhat), so will wait to see the GitHub actions failures before refactoring (if needed)
  • Do we want tests copying between different "drives"? I think these would be valuable.

• Use copyfile (with COPYFILE_CLONE_FORCE) when possible on macOS to clone the file while still keeping file locking logic intact
• Split common Unix logic into multiple functions that the macOS implementation uses parts of at different times
• Add string version of ResolveLinkTarget to save the allocation since part of the code needs it
• Need to add tests to check the file is actually cloned so we know if it works or not
@ghostghost added area-System.IO community-contribution Indicates that the PR has been added by a community member labels Dec 5, 2022
@ghost

ghost commented Dec 5, 2022

Copy link
Copy Markdown

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

Issue Details

Fixes #77835.

This should improve performance greatly for File.Copy when copying from one APFS volume to a different file in the volume (I think it's within a volume anyway). It will clone the file which is instant instead of copying all of the bytes, and if cloning doesn't work it will fall back to the normal method of copying.

Informative:

  • Uses copyfile (with COPYFILE_CLONE_FORCE) when possible on macOS to clone the file while still keeping file locking logic intact
  • Split common Unix logic into multiple functions that the macOS implementation uses parts of at different times
  • No shim needed since the ABI will not change. Precedent for doing it this way for macOS-specific functions: Fix setting creation date for OSX-like platforms #49555
  • Applies to all OSX-like platforms, not just macOS

Todo:

  • Need to add tests to check the file is actually cloned so we know if it works or not
  • The logic in this commit has not been tested thoroughly (but it has been tested somewhat), so will wait to see the GitHub actions issues before refactoring (if needed)
  • Is it a problem that it could theoretically delete the destination file and then fail for some other reason, leaving the destination file deleted when it may not have done so before (realistically this is extremely unlikely)?
Author:hamarb123
Assignees:-
Labels:

area-System.IO

Milestone:-

@hamarb123

Copy link
Copy Markdown
ContributorAuthor

Btw, this implementation is not done, I want to see if any tests fail first though.

• I missed setting StringMarshalling on the LibraryImport
• I missed partial on the CopyFile method implementations
• Apparently 'Both partial method declarations must be unsafe or neither may be unsafe'
• I love partial methods
• Move unsafe keyword into the OSX method rather than in method declaration
• Fix accessibility modifier of CopyFile in FileSystem.Unix.cs
• Fix indentation (should have used spaces)
• Import Microsoft.Win32.SafeHandles in FileSystem.CopyFile.OSX.cs file
• Fix for SA1205 'Partial elements should declare an access modifier'
• Fix typo in FileSystem.CopyFile.OtherUnix.cs of 'startedCopyFile'
• Fix missing openDst parameter code in StartCopyFile
• Fix not checking the nullability in StandardCopyFile, which led to 'Possible null reference argument for parameter'
• Add missing parameter value (openNewFile) to OpenCopyFileDstHandle
• Fix misnamed variable usages throughout some code
• Properly qualify Interop.ErrorInfo
• This should be the last fix for build issues for this set
@hamarb123

Copy link
Copy Markdown
ContributorAuthor

Sorry for all of the fix commits, I couldn't get build locally to work, so I copied the code into a new project to test it and must have missed some of the changes here.

@hamarb123
hamarb123 marked this pull request as draft December 5, 2022 21:44
• Add missing nullable ? for dstHandle (which obtains the file lock)
• Test failures were since copying a file onto itself should cause an error, this fixes that
…ain)
• The variable in the if statements weren't updated
• Handling for if readlink fails
…t#77835'
• The source path should have used sourceFullPath rather than destPath
@marek-safarmarek-safar added the os-mac-os-x macOS aka OSX label Dec 6, 2022
@hamarb123

hamarb123 commented Dec 6, 2022

Copy link
Copy Markdown
ContributorAuthor

@marek-safar, this applies to macOS, iOS, tvOS, and Mac Catalyst in case you wanted to add tags for those. Can you also 'assign' me if you're able? Thanks.

@hamarb123hamarb123 changed the title [WIP] Cloning files on macOS when possible instead of copying the whole file[WIP] Cloning files on OSX-like platforms when possible instead of copying the whole fileDec 6, 2022
@ghostghost closed this Jan 6, 2023
@ghost

ghost commented Jan 6, 2023

Copy link
Copy Markdown

Draft Pull Request was automatically closed for 30 days of inactivity. Please let us know if you'd like to reopen it.

@adamsitnik

Copy link
Copy Markdown
Member

@filipnavara is there any chance you could review this PR? You are currently the most experience person in this particular area.

@hamarb123

Copy link
Copy Markdown
ContributorAuthor

Is there a reason why 24 test runs have been cancelled so far? Do the tests need to be re-run?

@danmoseley

Copy link
Copy Markdown
Contributor

@hamarb123 it is dotnet/arcade#13774

@slang25

Copy link
Copy Markdown
Contributor

I'm really looking forward to this change! Will we be seeing it land in a .NET 8 preview?

@danmoseley

Copy link
Copy Markdown
Contributor

I'm really looking forward to this change! Will we be seeing it land in a .NET 8 preview?

If it's merged in the next month or two, it will be in .NET 8.

@hamarb123

Copy link
Copy Markdown
ContributorAuthor

I'm really looking forward to this change! Will we be seeing it land in a .NET 8 preview?

It's hopefully just a few more stages of review at most. I'm hoping it will make it to .NET 8 also.

@tmds

tmds commented Jun 13, 2023

Copy link
Copy Markdown
Member

Per @adamsitnik's earlier comment, macOS isn't benchmarked in the perf lab.

So before merging, we need some benchmark results that verify there is a performance gain.

@hamarb123 can you look at the documentation Adam shared, and run the benchmarks on your machine?

@hamarb123

hamarb123 commented Jun 13, 2023

Copy link
Copy Markdown
ContributorAuthor

Benchmark results:

  • CopyTo is ~1.9x faster for small files with the changes, and is usually ~180us for all the tests up to 1MB (~207us for 100MB test). Note this test also deletes the file as a part of the test, so simply copying would likely be faster again.
  • CopyToOverwrite is ~1.15x faster for small files with the changes, and is ~215us for all the tests.
  • Copying tests in System.IO.Tests.Perf_FileStream did not improve (haven't included them here), do we want to work out why and improve that API also?
  • The comparison was between the commit just before this comment which merges all of the new runtime changes, and the runtime repo with those same runtime changes but without this PR.

System.IO.Tests.Perf_File

With changes
MethodsizeMeanErrorStdDevMedianMinMaxAllocated
CopyTo512180.4 us3.42 us3.66 us180.0 us173.9 us186.3 us65 B
CopyToOverwrite512217.4 us4.17 us4.10 us217.2 us210.2 us225.9 us129 B
CopyTo4096175.9 us3.41 us3.19 us175.0 us171.2 us182.7 us65 B
CopyToOverwrite4096214.0 us2.41 us2.13 us213.4 us209.8 us218.2 us129 B
CopyTo1048576175.9 us2.41 us2.01 us175.9 us173.4 us179.8 us65 B
CopyToOverwrite1048576212.3 us3.97 us3.31 us212.0 us208.1 us218.1 us129 B
CopyTo104857600206.9 us2.32 us1.94 us206.6 us203.7 us210.8 us65 B
CopyToOverwrite104857600213.9 us3.58 us3.17 us214.2 us209.1 us218.0 us129 B
Without changes
MethodsizeMeanErrorStdDevMedianMinMaxAllocated
CopyTo512347.6 us12.46 us14.35 us344.8 us326.4 us374.2 us129 B
CopyToOverwrite512249.2 us5.56 us6.18 us248.7 us241.4 us261.1 us129 B
CopyTo4096347.1 us7.16 us7.36 us345.2 us335.4 us364.5 us129 B
CopyToOverwrite4096244.6 us3.07 us2.72 us243.9 us240.4 us248.4 us129 B
CopyTo10485761,313.5 us68.13 us72.90 us1,322.9 us1,191.9 us1,437.3 us133 B
CopyToOverwrite10485761,773.5 us321.63 us370.39 us1,730.5 us1,184.8 us2,432.6 us136 B
CopyTo10485760055,961.7 us4,954.14 us5,705.19 us53,346.9 us49,530.1 us65,915.7 us-
CopyToOverwrite10485760055,883.4 us5,472.86 us6,302.55 us54,004.8 us46,797.4 us66,152.4 us-

@tmds

tmds commented Jun 13, 2023

Copy link
Copy Markdown
Member

CopyToOverwrite is ~1.15x faster for small files with the changes

That's a pleasant surprise.

My expectation was to see a regression for small files with overwrite since we're making additional system calls and the benefit of cloning is low for small files.

@adamsitnikadamsitnik left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The code looks very good, I found only one thing that could be improved (PTAL at my comment).

The perf win is very impressive. I've benchmarked it on my old macBook:

BenchmarkDotNet=v0.13.2.2052-nightly, OS=macOS Monterey 12.6.5 (21G531) [Darwin 21.6.0]
Intel Core i7-5557U CPU 3.10GHz (Broadwell), 1 CPU, 4 logical and 2 physical cores
.NET SDK=8.0.100-preview.6.23312.5
[Host] : .NET 8.0.0 (8.0.23.30704), X64 RyuJIT AVX2
PR : .NET 8.0.0 (42.42.42.42424), X64 RyuJIT AVX2
main : .NET 8.0.0 (8.0.23.30704), X64 RyuJIT AVX2
MethodJobsizeMeanStdDevRatio
CopyToPR512438.7 μs106.58 μs0.75
CopyTomain512588.0 μs15.62 μs1.00
CopyToOverwritePR512298.8 μs6.18 μs0.79
CopyToOverwritemain512381.4 μs4.55 μs1.00
CopyToPR4096237.0 μs6.29 μs0.39
CopyTomain4096606.9 μs37.95 μs1.00
CopyToOverwritePR4096293.7 μs7.59 μs0.76
CopyToOverwritemain4096388.1 μs3.02 μs1.00
CopyToPR1048576239.9 μs10.95 μs0.09
CopyTomain10485762,567.9 μs107.61 μs1.00
CopyToOverwritePR1048576298.2 μs11.47 μs0.12
CopyToOverwritemain10485762,585.2 μs63.77 μs1.00
CopyToPR104857600247.0 μs8.00 μs0.001
CopyTomain104857600230,797.9 μs42,923.74 μs1.000
CopyToOverwritePR104857600291.4 μs6.45 μs0.001
CopyToOverwritemain104857600256,931.8 μs2,784.23 μs1.000

Great job @hamarb123 !!

@stephentoub

stephentoub commented Jun 13, 2023

Copy link
Copy Markdown
Member

The perf win is very impressive.

While this isn't a common scenario, in the name of completeness, what does the perf look like if, after copying, the benchmark then modifies the destination file in some way?

@adamsitnik

Copy link
Copy Markdown
Member

While this isn't a common scenario, in the name of completeness, what does the perf look like if, after copying, the benchmark then modifies the destination file in some way?

That is a very good question!

I've modified the benchmarks and just added File.WriteAllBytes(dest, File.ReadAllBytes(source)) right after the call to copy:

[Benchmark]
[Arguments(HalfKibibyte)]
[Arguments(FourKibibytes)]
[Arguments(OneMibibyte)]
[Arguments(HundredMibibytes)]
public void CopyTo_AndModify(int size)
{
File.Delete(_testFilePath);
File.Copy(_filesToRead[size], _testFilePath); // overwrite defaults to false
+ File.WriteAllBytes(_testFilePath, File.ReadAllBytes(_filesToRead[size]));
}
[Benchmark]
[Arguments(HalfKibibyte)]
[Arguments(FourKibibytes)]
[Arguments(OneMibibyte)]
[Arguments(HundredMibibytes)]
public void CopyToOverwrite_AndModify(int size)
{
File.Copy(_filesToRead[size], _testFilePath, overwrite: true);
+ File.WriteAllBytes(_testFilePath, File.ReadAllBytes(_filesToRead[size]));
}

We have still a major win for large files and no regression for smaller files

MethodJobsizeMeanStdDevRatio
CopyTo_AndModifyPR512533.1 μs16.83 μs0.71
CopyTo_AndModifymain512742.4 μs6.98 μs1.00
CopyToOverwrite_AndModifyPR512588.8 μs25.28 μs0.98
CopyToOverwrite_AndModifymain512593.9 μs10.35 μs1.00
CopyTo_AndModifyPR4096540.8 μs15.53 μs0.71
CopyTo_AndModifymain4096767.0 μs13.17 μs1.00
CopyToOverwrite_AndModifyPR4096590.7 μs19.49 μs1.00
CopyToOverwrite_AndModifymain4096594.7 μs5.73 μs1.00
CopyTo_AndModifyPR10485762,383.3 μs84.83 μs0.64
CopyTo_AndModifymain10485763,701.9 μs82.21 μs1.00
CopyToOverwrite_AndModifyPR10485762,358.2 μs69.96 μs0.69
CopyToOverwrite_AndModifymain10485763,412.3 μs87.54 μs1.00
CopyTo_AndModifyPR104857600167,426.8 μs18,209.30 μs0.61
CopyTo_AndModifymain104857600276,862.8 μs29,743.99 μs1.00
CopyToOverwrite_AndModifyPR104857600170,189.9 μs26,538.74 μs0.61
CopyToOverwrite_AndModifymain104857600287,618.2 μs48,952.24 μs1.00

@stephentoub

Copy link
Copy Markdown
Member

File.WriteAllBytes

WriteAllBytes opens the file as Create, which means it's going to effectively delete the destination and write a new file rather than modify the one that's there.

@adamsitnik

Copy link
Copy Markdown
Member

WriteAllBytes opens the file as Create, which means it's going to effectively delete the destination and write a new file rather than modify the one that's there.

Sorry, I forgot about that (BTW it just proves that we should really add #84532)

Updated benchmark:

privatebyte[]_oneKb=newbyte[1024];[Benchmark][Arguments(HalfKibibyte)][Arguments(FourKibibytes)][Arguments(OneMibibyte)][Arguments(HundredMibibytes)]publicvoidCopyTo_AndModify(intsize){File.Delete(_testFilePath);File.Copy(_filesToRead[size],_testFilePath);// overwrite defaults to falseusingFileStreamfs=File.OpenWrite(_testFilePath);fs.Write(_oneKb);}[Benchmark][Arguments(HalfKibibyte)][Arguments(FourKibibytes)][Arguments(OneMibibyte)][Arguments(HundredMibibytes)]publicvoidCopyToOverwrite_AndModify(intsize){File.Copy(_filesToRead[size],_testFilePath,overwrite:true);usingFileStreamfs=File.OpenWrite(_testFilePath);fs.Write(_oneKb);}

And the results:

BenchmarkDotNet=v0.13.2.2052-nightly, OS=macOS Monterey 12.6.5 (21G531) [Darwin 21.6.0]
Intel Core i7-5557U CPU 3.10GHz (Broadwell), 1 CPU, 4 logical and 2 physical cores
.NET SDK=8.0.100-preview.6.23312.5
[Host] : .NET 8.0.0 (8.0.23.30704), X64 RyuJIT AVX2
PR : .NET 8.0.0 (42.42.42.42424), X64 RyuJIT AVX2
main : .NET 8.0.0 (8.0.23.30704), X64 RyuJIT AVX2
IterationCount=40
MethodJobsizeMeanStdDevRatio
CopyTo_AndModifyPR512537.7 μs13.18 μs0.82
CopyTo_AndModifymain512661.3 μs9.98 μs1.00
CopyToOverwrite_AndModifyPR512549.1 μs16.54 μs1.15
CopyToOverwrite_AndModifymain512476.3 μs7.58 μs1.00
CopyTo_AndModifyPR4096669.8 μs20.57 μs1.01
CopyTo_AndModifymain4096662.4 μs11.60 μs1.00
CopyToOverwrite_AndModifyPR4096695.3 μs26.41 μs1.50
CopyToOverwrite_AndModifymain4096465.0 μs4.94 μs1.00
CopyTo_AndModifyPR1048576722.5 μs14.36 μs0.35
CopyTo_AndModifymain10485762,051.8 μs40.83 μs1.00
CopyToOverwrite_AndModifyPR1048576728.4 μs34.28 μs0.37
CopyToOverwrite_AndModifymain10485761,991.1 μs163.53 μs1.00
CopyTo_AndModifyPR104857600804.9 μs18.60 μs0.007
CopyTo_AndModifymain104857600109,415.3 μs8,542.98 μs1.000
CopyToOverwrite_AndModifyPR104857600762.1 μs26.40 μs0.007
CopyToOverwrite_AndModifymain104857600117,399.2 μs16,068.51 μs1.000

@adamsitnik
adamsitnik merged commit 8fb25a8 into dotnet:mainJun 13, 2023
@adamsitnikadamsitnik added this to the 8.0.0 milestone Jun 13, 2023
@stephentoub

Copy link
Copy Markdown
Member

BTW it just proves that we should really add #84532

Not really 😄

Updated benchmark

Thanks. So this does regress small overwrites that are subsequently modified.

@adamsitnik

Copy link
Copy Markdown
Member

Not really

Why? Do you believe that appending a buffer of bytes to an existing file is a rare scenario?

@danmoseley

Copy link
Copy Markdown
Contributor

Thanks for this nice change @hamarb123 . Would you like to tackle another issue? There are also a number marked api-approved that are available.

@stephentoub

stephentoub commented Jun 13, 2023

Copy link
Copy Markdown
Member

BTW it just proves that we should really add #84532

Not really

Why? Do you believe that appending a buffer of bytes to an existing file is a rare scenario?

I'm not commenting on the scenario of appending a buffer of bytes to an existing file. I'm commenting that a need to mutate a recently copied file as part of benchmarking a copy-on-write optimization and to achieve that writing two short lines of code doesn't prove that we need to add a new API.

@hamarb123

Copy link
Copy Markdown
ContributorAuthor

Thanks for this nice change @hamarb123 . Would you like to tackle another issue? There are also a number marked api-approved that are available.

Thanks! I'll have a look tomorrow (also known as a today at a sensible hour lol). I was also going to do the one for ReFS on Windows, was setting up a VM to test dev drives yesterday.

Copying tests in System.IO.Tests.Perf_FileStream did not improve (haven't included them here), do we want to work out why and improve that API also?

Just following up on this also. Presumably this API just does a block copy with the files. Is this something that's commonly used (I know I've never done it like this)? Because it doesn't seem to benefit from this, but maybe no-one uses it, idk.

@adamsitnik

Copy link
Copy Markdown
Member

Copying tests in System.IO.Tests.Perf_FileStream did not improve

IIRC The FileStream.CopyTo(Stream destination) has no special handling for cases where the destination stream is also a file stream.

Is this something that's commonly used

I don't have enough data to answer your question, but if you can provide an optimization that improves such scenario without introducing a lot of code complexity I am happy to review it.

@danmoseley

Copy link
Copy Markdown
Contributor

I was also going to do the one for ReFS on Windows

Ah yes, indeed that would be nice to get into .NET 8

@ghostghost locked as resolved and limited conversation to collaborators Jul 15, 2023
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-System.IOcommunity-contributionIndicates that the PR has been added by a community memberos-mac-os-xmacOS aka OSX

Projects

None yet

Development

Successfully merging this pull request may close these issues.

File.Copy: Clone file when possible on MacOS

10 participants

@hamarb123@adamsitnik@filipnavara@akoeplinger@stephentoub@danmoseley@jkotas@tmds@slang25@marek-safar
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); Clone files on OSX-like platforms when possible, instead of copying the whole file by hamarb123 · Pull Request #79243 · dotnet/runtime · GitHub
Skip to content

Clone files on OSX-like platforms when possible, instead of copying the whole file - #79243

Merged
adamsitnik merged 85 commits into
dotnet:mainfrom
hamarb123:main
Jun 13, 2023
Merged

Clone files on OSX-like platforms when possible, instead of copying the whole file#79243
adamsitnik merged 85 commits into
dotnet:mainfrom
hamarb123:main

Conversation

@hamarb123

@hamarb123hamarb123 commented Dec 5, 2022

Copy link
Copy Markdown
Contributor

Fixes#77835.

This should improve performance greatly for File.Copy when copying from one APFS volume to a different file in the volume (I think it's within a volume anyway). It will clone the file which is instant instead of copying all of the bytes, and if cloning doesn't work it will fall back to the normal method of copying.

Informative:

  • Uses clonefile when possible on macOS to clone the file while still keeping file locking logic intact
  • Split common Unix logic into multiple functions that the macOS implementation uses parts of at different times
  • No shim needed since the ABI will not change. Precedent for doing it this way for macOS-specific functions: Fix setting creation date for OSX-like platforms #49555
  • Applies to all OSX-like platforms (macOS, iOS, tvOS, and Mac Catalyst), not just macOS

Todo:

  • Need to add tests to check the file is actually cloned so we know if it works or not
  • The logic in this commit has not been tested thoroughly (but it has been tested somewhat), so will wait to see the GitHub actions failures before refactoring (if needed)
  • Do we want tests copying between different "drives"? I think these would be valuable.

• Use copyfile (with COPYFILE_CLONE_FORCE) when possible on macOS to clone the file while still keeping file locking logic intact
• Split common Unix logic into multiple functions that the macOS implementation uses parts of at different times
• Add string version of ResolveLinkTarget to save the allocation since part of the code needs it
• Need to add tests to check the file is actually cloned so we know if it works or not
@ghostghost added area-System.IO community-contribution Indicates that the PR has been added by a community member labels Dec 5, 2022
@ghost

ghost commented Dec 5, 2022

Copy link
Copy Markdown

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

Issue Details

Fixes #77835.

This should improve performance greatly for File.Copy when copying from one APFS volume to a different file in the volume (I think it's within a volume anyway). It will clone the file which is instant instead of copying all of the bytes, and if cloning doesn't work it will fall back to the normal method of copying.

Informative:

  • Uses copyfile (with COPYFILE_CLONE_FORCE) when possible on macOS to clone the file while still keeping file locking logic intact
  • Split common Unix logic into multiple functions that the macOS implementation uses parts of at different times
  • No shim needed since the ABI will not change. Precedent for doing it this way for macOS-specific functions: Fix setting creation date for OSX-like platforms #49555
  • Applies to all OSX-like platforms, not just macOS

Todo:

  • Need to add tests to check the file is actually cloned so we know if it works or not
  • The logic in this commit has not been tested thoroughly (but it has been tested somewhat), so will wait to see the GitHub actions issues before refactoring (if needed)
  • Is it a problem that it could theoretically delete the destination file and then fail for some other reason, leaving the destination file deleted when it may not have done so before (realistically this is extremely unlikely)?
Author:hamarb123
Assignees:-
Labels:

area-System.IO

Milestone:-

@hamarb123

Copy link
Copy Markdown
ContributorAuthor

Btw, this implementation is not done, I want to see if any tests fail first though.

• I missed setting StringMarshalling on the LibraryImport
• I missed partial on the CopyFile method implementations
• Apparently 'Both partial method declarations must be unsafe or neither may be unsafe'
• I love partial methods
• Move unsafe keyword into the OSX method rather than in method declaration
• Fix accessibility modifier of CopyFile in FileSystem.Unix.cs
• Fix indentation (should have used spaces)
• Import Microsoft.Win32.SafeHandles in FileSystem.CopyFile.OSX.cs file
• Fix for SA1205 'Partial elements should declare an access modifier'
• Fix typo in FileSystem.CopyFile.OtherUnix.cs of 'startedCopyFile'
• Fix missing openDst parameter code in StartCopyFile
• Fix not checking the nullability in StandardCopyFile, which led to 'Possible null reference argument for parameter'
• Add missing parameter value (openNewFile) to OpenCopyFileDstHandle
• Fix misnamed variable usages throughout some code
• Properly qualify Interop.ErrorInfo
• This should be the last fix for build issues for this set
@hamarb123

Copy link
Copy Markdown
ContributorAuthor

Sorry for all of the fix commits, I couldn't get build locally to work, so I copied the code into a new project to test it and must have missed some of the changes here.

@hamarb123
hamarb123 marked this pull request as draft December 5, 2022 21:44
• Add missing nullable ? for dstHandle (which obtains the file lock)
• Test failures were since copying a file onto itself should cause an error, this fixes that
…ain)
• The variable in the if statements weren't updated
• Handling for if readlink fails
…t#77835'
• The source path should have used sourceFullPath rather than destPath
@marek-safarmarek-safar added the os-mac-os-x macOS aka OSX label Dec 6, 2022
@hamarb123

hamarb123 commented Dec 6, 2022

Copy link
Copy Markdown
ContributorAuthor

@marek-safar, this applies to macOS, iOS, tvOS, and Mac Catalyst in case you wanted to add tags for those. Can you also 'assign' me if you're able? Thanks.

@hamarb123hamarb123 changed the title [WIP] Cloning files on macOS when possible instead of copying the whole file[WIP] Cloning files on OSX-like platforms when possible instead of copying the whole fileDec 6, 2022
@ghostghost closed this Jan 6, 2023
@ghost

ghost commented Jan 6, 2023

Copy link
Copy Markdown

Draft Pull Request was automatically closed for 30 days of inactivity. Please let us know if you'd like to reopen it.

@adamsitnik

Copy link
Copy Markdown
Member

@filipnavara is there any chance you could review this PR? You are currently the most experience person in this particular area.

@hamarb123

Copy link
Copy Markdown
ContributorAuthor

Is there a reason why 24 test runs have been cancelled so far? Do the tests need to be re-run?

@danmoseley

Copy link
Copy Markdown
Contributor

@hamarb123 it is dotnet/arcade#13774

@slang25

Copy link
Copy Markdown
Contributor

I'm really looking forward to this change! Will we be seeing it land in a .NET 8 preview?

@danmoseley

Copy link
Copy Markdown
Contributor

I'm really looking forward to this change! Will we be seeing it land in a .NET 8 preview?

If it's merged in the next month or two, it will be in .NET 8.

@hamarb123

Copy link
Copy Markdown
ContributorAuthor

I'm really looking forward to this change! Will we be seeing it land in a .NET 8 preview?

It's hopefully just a few more stages of review at most. I'm hoping it will make it to .NET 8 also.

@tmds

tmds commented Jun 13, 2023

Copy link
Copy Markdown
Member

Per @adamsitnik's earlier comment, macOS isn't benchmarked in the perf lab.

So before merging, we need some benchmark results that verify there is a performance gain.

@hamarb123 can you look at the documentation Adam shared, and run the benchmarks on your machine?

@hamarb123

hamarb123 commented Jun 13, 2023

Copy link
Copy Markdown
ContributorAuthor

Benchmark results:

  • CopyTo is ~1.9x faster for small files with the changes, and is usually ~180us for all the tests up to 1MB (~207us for 100MB test). Note this test also deletes the file as a part of the test, so simply copying would likely be faster again.
  • CopyToOverwrite is ~1.15x faster for small files with the changes, and is ~215us for all the tests.
  • Copying tests in System.IO.Tests.Perf_FileStream did not improve (haven't included them here), do we want to work out why and improve that API also?
  • The comparison was between the commit just before this comment which merges all of the new runtime changes, and the runtime repo with those same runtime changes but without this PR.

System.IO.Tests.Perf_File

With changes
MethodsizeMeanErrorStdDevMedianMinMaxAllocated
CopyTo512180.4 us3.42 us3.66 us180.0 us173.9 us186.3 us65 B
CopyToOverwrite512217.4 us4.17 us4.10 us217.2 us210.2 us225.9 us129 B
CopyTo4096175.9 us3.41 us3.19 us175.0 us171.2 us182.7 us65 B
CopyToOverwrite4096214.0 us2.41 us2.13 us213.4 us209.8 us218.2 us129 B
CopyTo1048576175.9 us2.41 us2.01 us175.9 us173.4 us179.8 us65 B
CopyToOverwrite1048576212.3 us3.97 us3.31 us212.0 us208.1 us218.1 us129 B
CopyTo104857600206.9 us2.32 us1.94 us206.6 us203.7 us210.8 us65 B
CopyToOverwrite104857600213.9 us3.58 us3.17 us214.2 us209.1 us218.0 us129 B
Without changes
MethodsizeMeanErrorStdDevMedianMinMaxAllocated
CopyTo512347.6 us12.46 us14.35 us344.8 us326.4 us374.2 us129 B
CopyToOverwrite512249.2 us5.56 us6.18 us248.7 us241.4 us261.1 us129 B
CopyTo4096347.1 us7.16 us7.36 us345.2 us335.4 us364.5 us129 B
CopyToOverwrite4096244.6 us3.07 us2.72 us243.9 us240.4 us248.4 us129 B
CopyTo10485761,313.5 us68.13 us72.90 us1,322.9 us1,191.9 us1,437.3 us133 B
CopyToOverwrite10485761,773.5 us321.63 us370.39 us1,730.5 us1,184.8 us2,432.6 us136 B
CopyTo10485760055,961.7 us4,954.14 us5,705.19 us53,346.9 us49,530.1 us65,915.7 us-
CopyToOverwrite10485760055,883.4 us5,472.86 us6,302.55 us54,004.8 us46,797.4 us66,152.4 us-

@tmds

tmds commented Jun 13, 2023

Copy link
Copy Markdown
Member

CopyToOverwrite is ~1.15x faster for small files with the changes

That's a pleasant surprise.

My expectation was to see a regression for small files with overwrite since we're making additional system calls and the benefit of cloning is low for small files.

@adamsitnikadamsitnik left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The code looks very good, I found only one thing that could be improved (PTAL at my comment).

The perf win is very impressive. I've benchmarked it on my old macBook:

BenchmarkDotNet=v0.13.2.2052-nightly, OS=macOS Monterey 12.6.5 (21G531) [Darwin 21.6.0]
Intel Core i7-5557U CPU 3.10GHz (Broadwell), 1 CPU, 4 logical and 2 physical cores
.NET SDK=8.0.100-preview.6.23312.5
[Host] : .NET 8.0.0 (8.0.23.30704), X64 RyuJIT AVX2
PR : .NET 8.0.0 (42.42.42.42424), X64 RyuJIT AVX2
main : .NET 8.0.0 (8.0.23.30704), X64 RyuJIT AVX2
MethodJobsizeMeanStdDevRatio
CopyToPR512438.7 μs106.58 μs0.75
CopyTomain512588.0 μs15.62 μs1.00
CopyToOverwritePR512298.8 μs6.18 μs0.79
CopyToOverwritemain512381.4 μs4.55 μs1.00
CopyToPR4096237.0 μs6.29 μs0.39
CopyTomain4096606.9 μs37.95 μs1.00
CopyToOverwritePR4096293.7 μs7.59 μs0.76
CopyToOverwritemain4096388.1 μs3.02 μs1.00
CopyToPR1048576239.9 μs10.95 μs0.09
CopyTomain10485762,567.9 μs107.61 μs1.00
CopyToOverwritePR1048576298.2 μs11.47 μs0.12
CopyToOverwritemain10485762,585.2 μs63.77 μs1.00
CopyToPR104857600247.0 μs8.00 μs0.001
CopyTomain104857600230,797.9 μs42,923.74 μs1.000
CopyToOverwritePR104857600291.4 μs6.45 μs0.001
CopyToOverwritemain104857600256,931.8 μs2,784.23 μs1.000

Great job @hamarb123 !!

@stephentoub

stephentoub commented Jun 13, 2023

Copy link
Copy Markdown
Member

The perf win is very impressive.

While this isn't a common scenario, in the name of completeness, what does the perf look like if, after copying, the benchmark then modifies the destination file in some way?

@adamsitnik

Copy link
Copy Markdown
Member

While this isn't a common scenario, in the name of completeness, what does the perf look like if, after copying, the benchmark then modifies the destination file in some way?

That is a very good question!

I've modified the benchmarks and just added File.WriteAllBytes(dest, File.ReadAllBytes(source)) right after the call to copy:

[Benchmark]
[Arguments(HalfKibibyte)]
[Arguments(FourKibibytes)]
[Arguments(OneMibibyte)]
[Arguments(HundredMibibytes)]
public void CopyTo_AndModify(int size)
{
File.Delete(_testFilePath);
File.Copy(_filesToRead[size], _testFilePath); // overwrite defaults to false
+ File.WriteAllBytes(_testFilePath, File.ReadAllBytes(_filesToRead[size]));
}
[Benchmark]
[Arguments(HalfKibibyte)]
[Arguments(FourKibibytes)]
[Arguments(OneMibibyte)]
[Arguments(HundredMibibytes)]
public void CopyToOverwrite_AndModify(int size)
{
File.Copy(_filesToRead[size], _testFilePath, overwrite: true);
+ File.WriteAllBytes(_testFilePath, File.ReadAllBytes(_filesToRead[size]));
}

We have still a major win for large files and no regression for smaller files

MethodJobsizeMeanStdDevRatio
CopyTo_AndModifyPR512533.1 μs16.83 μs0.71
CopyTo_AndModifymain512742.4 μs6.98 μs1.00
CopyToOverwrite_AndModifyPR512588.8 μs25.28 μs0.98
CopyToOverwrite_AndModifymain512593.9 μs10.35 μs1.00
CopyTo_AndModifyPR4096540.8 μs15.53 μs0.71
CopyTo_AndModifymain4096767.0 μs13.17 μs1.00
CopyToOverwrite_AndModifyPR4096590.7 μs19.49 μs1.00
CopyToOverwrite_AndModifymain4096594.7 μs5.73 μs1.00
CopyTo_AndModifyPR10485762,383.3 μs84.83 μs0.64
CopyTo_AndModifymain10485763,701.9 μs82.21 μs1.00
CopyToOverwrite_AndModifyPR10485762,358.2 μs69.96 μs0.69
CopyToOverwrite_AndModifymain10485763,412.3 μs87.54 μs1.00
CopyTo_AndModifyPR104857600167,426.8 μs18,209.30 μs0.61
CopyTo_AndModifymain104857600276,862.8 μs29,743.99 μs1.00
CopyToOverwrite_AndModifyPR104857600170,189.9 μs26,538.74 μs0.61
CopyToOverwrite_AndModifymain104857600287,618.2 μs48,952.24 μs1.00

@stephentoub

Copy link
Copy Markdown
Member

File.WriteAllBytes

WriteAllBytes opens the file as Create, which means it's going to effectively delete the destination and write a new file rather than modify the one that's there.

@adamsitnik

Copy link
Copy Markdown
Member

WriteAllBytes opens the file as Create, which means it's going to effectively delete the destination and write a new file rather than modify the one that's there.

Sorry, I forgot about that (BTW it just proves that we should really add #84532)

Updated benchmark:

privatebyte[]_oneKb=newbyte[1024];[Benchmark][Arguments(HalfKibibyte)][Arguments(FourKibibytes)][Arguments(OneMibibyte)][Arguments(HundredMibibytes)]publicvoidCopyTo_AndModify(intsize){File.Delete(_testFilePath);File.Copy(_filesToRead[size],_testFilePath);// overwrite defaults to falseusingFileStreamfs=File.OpenWrite(_testFilePath);fs.Write(_oneKb);}[Benchmark][Arguments(HalfKibibyte)][Arguments(FourKibibytes)][Arguments(OneMibibyte)][Arguments(HundredMibibytes)]publicvoidCopyToOverwrite_AndModify(intsize){File.Copy(_filesToRead[size],_testFilePath,overwrite:true);usingFileStreamfs=File.OpenWrite(_testFilePath);fs.Write(_oneKb);}

And the results:

BenchmarkDotNet=v0.13.2.2052-nightly, OS=macOS Monterey 12.6.5 (21G531) [Darwin 21.6.0]
Intel Core i7-5557U CPU 3.10GHz (Broadwell), 1 CPU, 4 logical and 2 physical cores
.NET SDK=8.0.100-preview.6.23312.5
[Host] : .NET 8.0.0 (8.0.23.30704), X64 RyuJIT AVX2
PR : .NET 8.0.0 (42.42.42.42424), X64 RyuJIT AVX2
main : .NET 8.0.0 (8.0.23.30704), X64 RyuJIT AVX2
IterationCount=40
MethodJobsizeMeanStdDevRatio
CopyTo_AndModifyPR512537.7 μs13.18 μs0.82
CopyTo_AndModifymain512661.3 μs9.98 μs1.00
CopyToOverwrite_AndModifyPR512549.1 μs16.54 μs1.15
CopyToOverwrite_AndModifymain512476.3 μs7.58 μs1.00
CopyTo_AndModifyPR4096669.8 μs20.57 μs1.01
CopyTo_AndModifymain4096662.4 μs11.60 μs1.00
CopyToOverwrite_AndModifyPR4096695.3 μs26.41 μs1.50
CopyToOverwrite_AndModifymain4096465.0 μs4.94 μs1.00
CopyTo_AndModifyPR1048576722.5 μs14.36 μs0.35
CopyTo_AndModifymain10485762,051.8 μs40.83 μs1.00
CopyToOverwrite_AndModifyPR1048576728.4 μs34.28 μs0.37
CopyToOverwrite_AndModifymain10485761,991.1 μs163.53 μs1.00
CopyTo_AndModifyPR104857600804.9 μs18.60 μs0.007
CopyTo_AndModifymain104857600109,415.3 μs8,542.98 μs1.000
CopyToOverwrite_AndModifyPR104857600762.1 μs26.40 μs0.007
CopyToOverwrite_AndModifymain104857600117,399.2 μs16,068.51 μs1.000

@adamsitnik
adamsitnik merged commit 8fb25a8 into dotnet:mainJun 13, 2023
@adamsitnikadamsitnik added this to the 8.0.0 milestone Jun 13, 2023
@stephentoub

Copy link
Copy Markdown
Member

BTW it just proves that we should really add #84532

Not really 😄

Updated benchmark

Thanks. So this does regress small overwrites that are subsequently modified.

@adamsitnik

Copy link
Copy Markdown
Member

Not really

Why? Do you believe that appending a buffer of bytes to an existing file is a rare scenario?

@danmoseley

Copy link
Copy Markdown
Contributor

Thanks for this nice change @hamarb123 . Would you like to tackle another issue? There are also a number marked api-approved that are available.

@stephentoub

stephentoub commented Jun 13, 2023

Copy link
Copy Markdown
Member

BTW it just proves that we should really add #84532

Not really

Why? Do you believe that appending a buffer of bytes to an existing file is a rare scenario?

I'm not commenting on the scenario of appending a buffer of bytes to an existing file. I'm commenting that a need to mutate a recently copied file as part of benchmarking a copy-on-write optimization and to achieve that writing two short lines of code doesn't prove that we need to add a new API.

@hamarb123

Copy link
Copy Markdown
ContributorAuthor

Thanks for this nice change @hamarb123 . Would you like to tackle another issue? There are also a number marked api-approved that are available.

Thanks! I'll have a look tomorrow (also known as a today at a sensible hour lol). I was also going to do the one for ReFS on Windows, was setting up a VM to test dev drives yesterday.

Copying tests in System.IO.Tests.Perf_FileStream did not improve (haven't included them here), do we want to work out why and improve that API also?

Just following up on this also. Presumably this API just does a block copy with the files. Is this something that's commonly used (I know I've never done it like this)? Because it doesn't seem to benefit from this, but maybe no-one uses it, idk.

@adamsitnik

Copy link
Copy Markdown
Member

Copying tests in System.IO.Tests.Perf_FileStream did not improve

IIRC The FileStream.CopyTo(Stream destination) has no special handling for cases where the destination stream is also a file stream.

Is this something that's commonly used

I don't have enough data to answer your question, but if you can provide an optimization that improves such scenario without introducing a lot of code complexity I am happy to review it.

@danmoseley

Copy link
Copy Markdown
Contributor

I was also going to do the one for ReFS on Windows

Ah yes, indeed that would be nice to get into .NET 8

@ghostghost locked as resolved and limited conversation to collaborators Jul 15, 2023
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-System.IOcommunity-contributionIndicates that the PR has been added by a community memberos-mac-os-xmacOS aka OSX

Projects

None yet

Development

Successfully merging this pull request may close these issues.

File.Copy: Clone file when possible on MacOS

10 participants

@hamarb123@adamsitnik@filipnavara@akoeplinger@stephentoub@danmoseley@jkotas@tmds@slang25@marek-safar