-
Notifications
You must be signed in to change notification settings - Fork 117
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
FAT32 io.Copy performance #130
Comments
Hi @abarisani
Thanks! It is fully OSS without corporate backing, so push it ahead whenever possible.
That is possible. Do you have any benchmarks? It could be the processing, it could be the read from the file, it could be the underlying filesystem (you said you were opening a FAT32 file, and not a raw block device).
Do you mean the blocksize of the filesystem on the file image? Or the blocksize of the underlying filesystem on which the image exists? I assume you mean the former, as the latter would be out of scope and handled by the OS on which it is running.
Agreed. Care to submit a PR? |
We are not running any OS and mapping util.File directly on a block device which represents a microSD card. Therefore there is no underlying file system and we are not using partitions for now. So the speed is not affected by the OS or a filesystem. The speed decreases incrementally throughout the write, this is unrelated to the SD card. I performed a similar test also on Linux and accessing the SD card directly through /dev and the performance is also pretty slow. This only happens when writing and not when reading. For the second issue the block size I am referring to is the one passed to Create, I am not sure if my expectation for it to be used in every WriteAt is correct or not, I can work this around if I am mistaken. The lack of error trapping remains and happy to submit a PR for it. |
Correction: I am not noting the incremental slowdown in Linux, only within my tamago integration (where I read the body from an HTTP client), so I need to investigate what is going on there. On Linux the performance is faster (though still pretty slow at < 1 MB/s) but at least it's always linear. Closing the issue until further investigation on my side confirms something wrong with go-diskfs rather than my integration of it. Thanks! |
The following test program performs a file write, under Linux, on a file-backed FAT32 filesystem. The test program simply wraps the underlying block device to trace its A test against a 32GB microSD card (seen as a block device at When performing such test I see a sequence of writes possibly responsible for the poor performance:
There are 459 transfers of 32768 and 1 transfer of 23239 bytes which account for the actual 15063751 bytes of the file being copied. Of course more than the file itself is written under any filesystem scheme to account for metadata, however the list of metadata writes feels excessive. If I repeat the same test on a 50MB FAT32 filesystem, transferring the same 15063751 bytes (~15M) PDF, I see:
If I repeat the same test on a 100MB FAT32 filesystem, transferring the same 15063751 bytes (~15M) PDF, I see:
Again I am not a FAT32 expert so I am not sure what I am seeing here. package main
import (
"io"
"log"
"os"
"github.com/diskfs/go-diskfs/filesystem/fat32"
)
const (
blockSize = 512
fsSize = 1048576 * 100
fsPath = "/tmp/fat32.bin"
)
type BlockDevice struct {
file *os.File
}
func (b *BlockDevice) Init(path string) (err error) {
b.file, err = os.OpenFile(path, os.O_CREATE|os.O_EXCL|os.O_RDWR, 0600)
return
}
func (b BlockDevice) ReadAt(p []byte, off int64) (n int, err error) {
n, err = b.file.ReadAt(p, off)
log.Printf("ReadAt offset:%d size:%d read:%d", off, len(p), n)
return
}
func (b BlockDevice) WriteAt(p []byte, off int64) (n int, err error) {
n, err = b.file.WriteAt(p, off)
log.Printf("WriteAt offset:%d size:%d written:%d", off, len(p), n)
return
}
func (b BlockDevice) Seek(off int64, whence int) (int64, error) {
log.Printf("Seek offset:%d whence:%d", off, whence)
return b.file.Seek(off, whence)
}
func main() {
dev := BlockDevice{}
if err := dev.Init(fsPath); err != nil {
panic(err)
}
fs, err := fat32.Create(dev, fsSize, 0, blockSize, "godiskfs")
if err != nil {
panic(err)
}
input, err := os.OpenFile("/tmp/example.pdf", os.O_RDONLY, 0600)
if err != nil {
panic(err)
}
defer input.Close()
output, err := fs.OpenFile("/example.pdf", os.O_RDWR|os.O_CREATE)
if err != nil {
panic(err)
}
defer output.Close()
_, err = io.Copy(output, input)
if err != nil {
panic(err)
}
} |
Excellent issue, sample code to recreate it and everything. I ran that code, sorted the results, here is what I get for repetitions:
Not much of a mystery here, but it does take understanding how the fat32 filesystem works. As you write your file out, it uses allocated clusters. As soon as the number runs out, it needs to allocate some more, which means:
The sectors it uses are:
So it does all make sense (except for the Backup Boot Sector, which surprises me). The question is, what can be done to make it more efficient? The first thing is to use CopyBuffer instead of This is the same thing a kernel (or filesystem driver) has to deal with when you do:
You are streaming the file, the kernel has no way of knowing how big that will be. I believe there are whole areas of filesystem management that get into that. The other thing you could do is use an |
Pre-allocation would be nice. It is not clear to me why the transfers to the primary FAT table are so large, as well as dependent on the overall fileystem size. Could you shed some light on this? |
Because it has the whole FAT table in memory, and so it writes it to the underlying disk, twice (primary and backup), with each change.
Calculate it roughly as:
If you have a larger disk, you need more clusters to store data, and hence the FAT table has to be larger. There is an argument to be made for writing out just the changed sectors of the FAT table. E.g. if there are 1599 sectors for the FAT table, and you changed 1, write that one (primary and backup). But you do run a risk of getting them out of sync. I would be open to it, but we need to think it through. |
I confirm that |
hm, does it possible not write immediately fat table on disk, but write memory copy of table in per duration interval? something like CommitInterval/SyncInterval ? Or this does not help at all ? |
I don't know how operating systems generally handle this so I am not sure how I can provide insights. For my specific use case if there would be the option to keep the table copy in memory and only write it once the file hits |
You are getting into kernel and filesystem design, which is not my expertise. I know enough to cause trouble. As far as I understand it, there usually is some form of cache of anything to write (not just the system metadata, but any writes to disk), which are flushed out either when the amount of change in cache becomes too big, or after some period of time, or when orderly shutdown happens. Separately, you can also force a sync at any given moment. This is not a trivial thing to handle here, but, if properly planned out, I am open to it. |
There are several layers of caching on any system, however I think that when evaluating a pure Go filesystem library disk or OS caching are not relevant. I think the performance should be optimized, if possible, ignoring such layers. It seems to me that the following optimizations are possible and non mutually exclusive:
An alternative strategy, which you suggested, is to add your own layer of caching and keeping the fat table in memory for updates before committing it to the disk, this feels possible but only with the assumption (or better enforcement via mutex) of locking the filesystem for the duration of the write. |
Makes sense. Those certainly are simpler. I don't know that I would do
We can do |
I agree, |
#255 gives us ~20 times speed up for 1G+ size |
Hello there.
I am evaluating go-diskfs (nice project!) for use with our tamago framework to port interlock to bare metal. The integration is promising however I am stumbling upon some issues.
First of all while using
io.Copy
on a FAT32 file opened withOpenFile
I notice that performance is pretty slow, it seems that write speed decreases as the file gets larger, I am using something similar to the following:I think this relates to #110 (comment)
Additionally I notice that writes to the underlying FileSystem can be of sizes which are not a multiple of its block size, I am not sure if this is intended or not.
Despite this being correct or not, for sure it's problematic that
WriteAt
errors are not all trapped, for example my custom block device (which reads/writes from an SD card) does not allow writes which are not a multiple of block size, theWriteAt
error was raised but it's never caught infat32.allocateSpace
(also not in some instances offat32.Create
).So my issues are the following:
WriteAt
to be invoked without honoring theFileSystem
block size ?WriteAt
errors should be trapped.Thanks!
The text was updated successfully, but these errors were encountered: