Hey! With our brief discussion yesterday, I wanted to better explain what exactly I'm working towards with #160. I would also like to use this issue as a more focused space to discuss the overall design instead of the implementation for just MacOS.
My overall goal is to make copy as fast as possible by taking advantage of features provided by different operating systems.
About Previous PRs
I'm thinking of reworking my previous pull request to be for the design that allows copy to use these features. Then, I can open up a new pull request for each single optimization.
You made some very good points about the problems with my original design, and looking back on it, it focused too much on providing abstraction/flexibility. Yours is a lot simpler, but I feel that it sees the feature as more of an afterthought than a core part of copy. I would like work together on a new one that is: simpler, well-integrated, and able to work within the different limitations of each supported platform's API.
Considerations
Why Fallback
Not all of the platform-specific optimizations are equally as good. They also can't be combined.
In order of best performance first, we have:
- Copy on write (let the kernel make a new link to the file)
- In-kernel copying (avoid O(n) context switching)
- Read and write copies (the current way)
The end result of copying the file is the same, but the way they work is different. Copy-on-write has limitations that the others do not. Then, we also need to consider copying when the source is fs.FS or when using WrapReader since they can't use any of these optimizations.
Because of that, I think taking a fallback approach would be ideal. If copy-on-write can't be used, use in-kernel copying. If in-kernel copying can't be used, do read and write copying.
It's also better to follow the easier to ask for forgiveness approach here. By not checking if something is possible in Go code first and just trying it, it's both less code and faster—the kernel will do the checks itself whether or not we check it in Go first.
Platform API Inconsistencies
Linux and MacOS have different ways of exposing the copy-on-write file API. Depending on that, using them has to be in one of two places:
-
Before files are opened, as with clonefile. This is because they only take file paths or a dirfd.
-
After files are opened, when a file descriptor is held. This is because they only take file descriptors, like for sendfile and Linux copy-on-write.
Optimization 1: Darwin Copy-on-Write
Using clonefile.
Manpage
Considerations:
- It uses file paths.
- It keeps file times.
- It removes setuid and setgid permissions.
- It requires filesystem support.
- It requires src and dest are on the same filesystem.
- It fails if the dest file already exists.
Optimization 2: Linux Copy-on-Write
Using the FICLONE ioctl.
Manpage
Considerations:
- It uses file descriptors.
- It requires filesystem support.
- It requires src and dest are on the same filesystem.
- (Probably more to find when testing.)
Optimization 3: Linux In-Kernel Copying
Using the sendfile syscall.
Manpage
Considerations:
- It's still copying the bytes on the storage.
- It will work across filesystems.
Remaining Questions
Is it enabled by default?
Since these are optimizations, they shouldn't change how copy works. Because of that, it would be possible to enable them by default.
However: it's impossible to know how every user is calling Copy, and there's always a chance that the optimization will cause a regression in their use case.
That can be avoided by making it an Option that they enable, or by following Go' convention to create a new copy/v2 package for breaking changes.
How to test the specific optimizations?
We would need a way to test that copying with one of the optimizations has the same expected behavior as doing read-and-write copying.
I did this in #160 with a go test build tag, but maybe there's a better way?
Hey! With our brief discussion yesterday, I wanted to better explain what exactly I'm working towards with #160. I would also like to use this issue as a more focused space to discuss the overall design instead of the implementation for just MacOS.
My overall goal is to make
copyas fast as possible by taking advantage of features provided by different operating systems.About Previous PRs
I'm thinking of reworking my previous pull request to be for the design that allows
copyto use these features. Then, I can open up a new pull request for each single optimization.You made some very good points about the problems with my original design, and looking back on it, it focused too much on providing abstraction/flexibility. Yours is a lot simpler, but I feel that it sees the feature as more of an afterthought than a core part of
copy. I would like work together on a new one that is: simpler, well-integrated, and able to work within the different limitations of each supported platform's API.Considerations
Why Fallback
Not all of the platform-specific optimizations are equally as good. They also can't be combined.
In order of best performance first, we have:
The end result of copying the file is the same, but the way they work is different. Copy-on-write has limitations that the others do not. Then, we also need to consider copying when the source is
fs.FSor when using WrapReader since they can't use any of these optimizations.Because of that, I think taking a fallback approach would be ideal. If copy-on-write can't be used, use in-kernel copying. If in-kernel copying can't be used, do read and write copying.
It's also better to follow the easier to ask for forgiveness approach here. By not checking if something is possible in Go code first and just trying it, it's both less code and faster—the kernel will do the checks itself whether or not we check it in Go first.
Platform API Inconsistencies
Linux and MacOS have different ways of exposing the copy-on-write file API. Depending on that, using them has to be in one of two places:
Before files are opened, as with
clonefile. This is because they only take file paths or adirfd.After files are opened, when a file descriptor is held. This is because they only take file descriptors, like for
sendfileand Linux copy-on-write.Optimization 1: Darwin Copy-on-Write
Using
clonefile.Manpage
Considerations:
Optimization 2: Linux Copy-on-Write
Using the
FICLONEioctl.Manpage
Considerations:
Optimization 3: Linux In-Kernel Copying
Using the
sendfilesyscall.Manpage
Considerations:
Remaining Questions
Is it enabled by default?
Since these are optimizations, they shouldn't change how
copyworks. Because of that, it would be possible to enable them by default.However: it's impossible to know how every user is calling
Copy, and there's always a chance that the optimization will cause a regression in their use case.That can be avoided by making it an
Optionthat they enable, or by following Go' convention to create a newcopy/v2package for breaking changes.How to test the specific optimizations?
We would need a way to test that copying with one of the optimizations has the same expected behavior as doing read-and-write copying.
I did this in #160 with a
go testbuild tag, but maybe there's a better way?