Skip to content

Commit 970b1c0

Browse files
panjf2000gopherbot
authored andcommitted
os: increase the amount of data transfer for sendfile(2) to reduce syscalls
For the moment, Go calls sendfile(2) to transfer at most 4MB at a time while sendfile(2) actually allows a larger amount of data on one call. To reduce system calls of sendfile(2) during data copying, we should specify the number of bytes to copy as large as possible. This optimization is especially advantageous for bulky file-to-file copies, it would lead to a performance boost, the magnitude of this performance increase may not be very exciting, but it can also cut down the CPU overhead by decreasing the number of system calls. This is also how we've done in sendfile_windows.go with TransmitFile. goos: linux goarch: amd64 pkg: os cpu: DO-Premium-AMD │ old │ new │ │ sec/op │ sec/op vs base │ SendFile-8 1.135 ± 4% 1.052 ± 3% -7.24% (p=0.000 n=10) │ old │ new │ │ B/s │ B/s vs base │ SendFile-8 902.5Mi ± 4% 973.0Mi ± 3% +7.81% (p=0.000 n=10) │ old │ new │ │ B/op │ B/op vs base │ SendFile-8 272.0 ± 0% 272.0 ± 0% ~ (p=1.000 n=10) ¹ ¹ all samples are equal │ old │ new │ │ allocs/op │ allocs/op vs base │ SendFile-8 20.00 ± 0% 20.00 ± 0% ~ (p=1.000 n=10) ¹ ¹ all samples are equal Change-Id: Ib4d4c6bc693e23db24697363b29226f0c9776bb0 Reviewed-on: https://go-review.googlesource.com/c/go/+/605235 LUCI-TryBot-Result: Go LUCI <[email protected]> Auto-Submit: Ian Lance Taylor <[email protected]> TryBot-Result: Gopher Robot <[email protected]> Reviewed-by: Jorropo <[email protected]> Run-TryBot: Andy Pan <[email protected]> Reviewed-by: Ian Lance Taylor <[email protected]> Reviewed-by: Carlos Amedee <[email protected]>
1 parent dc5389d commit 970b1c0

7 files changed

+89
-21
lines changed

src/internal/poll/sendfile_bsd.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,10 @@ import "syscall"
1010

1111
// maxSendfileSize is the largest chunk size we ask the kernel to copy
1212
// at a time.
13-
const maxSendfileSize int = 4 << 20
13+
// sendfile(2)s on *BSD and Darwin don't have a limit on the size of
14+
// data to copy at a time, we pick the typical SSIZE_MAX on 32-bit systems,
15+
// which ought to be sufficient for all practical purposes.
16+
const maxSendfileSize int = 1<<31 - 1
1417

1518
// SendFile wraps the sendfile system call.
1619
func SendFile(dstFD *FD, src int, pos, remain int64) (written int64, err error, handled bool) {

src/internal/poll/sendfile_linux.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@ import "syscall"
88

99
// maxSendfileSize is the largest chunk size we ask the kernel to copy
1010
// at a time.
11-
const maxSendfileSize int = 4 << 20
11+
// sendfile(2) on Linux will transfer at most 0x7ffff000 (2,147,479,552)
12+
// bytes, which is true on both 32-bit and 64-bit systems.
13+
// See https://man7.org/linux/man-pages/man2/sendfile.2.html#NOTES for details.
14+
const maxSendfileSize int = 0x7ffff000
1215

1316
// SendFile wraps the sendfile system call.
1417
func SendFile(dstFD *FD, src int, remain int64) (written int64, err error, handled bool) {

src/internal/poll/sendfile_solaris.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,10 @@ import "syscall"
1515

1616
// maxSendfileSize is the largest chunk size we ask the kernel to copy
1717
// at a time.
18-
const maxSendfileSize int = 4 << 20
18+
// sendfile(2)s on SunOS derivatives don't have a limit on the size of
19+
// data to copy at a time, we pick the typical SSIZE_MAX on 32-bit systems,
20+
// which ought to be sufficient for all practical purposes.
21+
const maxSendfileSize int = 1<<31 - 1
1922

2023
// SendFile wraps the sendfile system call.
2124
func SendFile(dstFD *FD, src int, pos, remain int64) (written int64, err error, handled bool) {

src/os/readfrom_linux_test.go

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -345,18 +345,20 @@ func hookCopyFileRange(t *testing.T) (hook *copyFileHook, name string) {
345345
return
346346
}
347347

348-
func hookSendFileOverCopyFileRange(t *testing.T) (hook *copyFileHook, name string) {
349-
name = "hookSendFileOverCopyFileRange"
348+
func hookSendFileOverCopyFileRange(t *testing.T) (*copyFileHook, string) {
349+
return hookSendFileTB(t), "hookSendFileOverCopyFileRange"
350+
}
350351

352+
func hookSendFileTB(tb testing.TB) *copyFileHook {
351353
// Disable poll.CopyFileRange to force the fallback to poll.SendFile.
352354
originalCopyFileRange := *PollCopyFileRangeP
353355
*PollCopyFileRangeP = func(dst, src *poll.FD, remain int64) (written int64, handled bool, err error) {
354356
return 0, false, nil
355357
}
356358

357-
hook = new(copyFileHook)
359+
hook := new(copyFileHook)
358360
orig := poll.TestHookDidSendFile
359-
t.Cleanup(func() {
361+
tb.Cleanup(func() {
360362
*PollCopyFileRangeP = originalCopyFileRange
361363
poll.TestHookDidSendFile = orig
362364
})
@@ -368,7 +370,7 @@ func hookSendFileOverCopyFileRange(t *testing.T) (hook *copyFileHook, name strin
368370
hook.err = err
369371
hook.handled = handled
370372
}
371-
return
373+
return hook
372374
}
373375

374376
func hookSpliceFile(t *testing.T) *spliceFileHook {

src/os/readfrom_sendfile_test.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
// Copyright 2024 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
//go:build linux || solaris
6+
7+
package os_test
8+
9+
import (
10+
"io"
11+
. "os"
12+
"testing"
13+
)
14+
15+
func BenchmarkSendFile(b *testing.B) {
16+
hook := hookSendFileTB(b)
17+
18+
// 1 GiB file size for copy.
19+
const fileSize = 1 << 30
20+
21+
src, _ := createTempFile(b, "benchmark-sendfile-src", int64(fileSize))
22+
dst, err := CreateTemp(b.TempDir(), "benchmark-sendfile-dst")
23+
if err != nil {
24+
b.Fatalf("failed to create temporary file of destination: %v", err)
25+
}
26+
b.Cleanup(func() {
27+
dst.Close()
28+
})
29+
30+
b.ReportAllocs()
31+
b.SetBytes(int64(fileSize))
32+
b.ResetTimer()
33+
34+
for i := 0; i <= b.N; i++ {
35+
sent, err := io.Copy(dst, src)
36+
37+
if err != nil {
38+
b.Fatalf("failed to copy data: %v", err)
39+
}
40+
if !hook.called {
41+
b.Fatalf("should have called the sendfile(2)")
42+
}
43+
if sent != int64(fileSize) {
44+
b.Fatalf("sent %d bytes, want %d", sent, fileSize)
45+
}
46+
47+
// Rewind the files for the next iteration.
48+
if _, err := src.Seek(0, io.SeekStart); err != nil {
49+
b.Fatalf("failed to rewind the source file: %v", err)
50+
}
51+
if _, err := dst.Seek(0, io.SeekStart); err != nil {
52+
b.Fatalf("failed to rewind the destination file: %v", err)
53+
}
54+
}
55+
}

src/os/readfrom_solaris_test.go

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,12 +38,14 @@ func newSendfileTest(t *testing.T, size int64) (dst, src *File, data []byte, hoo
3838
return
3939
}
4040

41-
func hookSendFile(t *testing.T) (hook *copyFileHook, name string) {
42-
name = "hookSendFile"
41+
func hookSendFile(t *testing.T) (*copyFileHook, string) {
42+
return hookSendFileTB(t), "hookSendFile"
43+
}
4344

44-
hook = new(copyFileHook)
45+
func hookSendFileTB(tb testing.TB) *copyFileHook {
46+
hook := new(copyFileHook)
4547
orig := poll.TestHookDidSendFile
46-
t.Cleanup(func() {
48+
tb.Cleanup(func() {
4749
poll.TestHookDidSendFile = orig
4850
})
4951
poll.TestHookDidSendFile = func(dstFD *poll.FD, src int, written int64, err error, handled bool) {
@@ -54,5 +56,5 @@ func hookSendFile(t *testing.T) (hook *copyFileHook, name string) {
5456
hook.err = err
5557
hook.handled = handled
5658
}
57-
return
59+
return hook
5860
}

src/os/readfrom_unix_test.go

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -428,28 +428,28 @@ type copyFileHook struct {
428428
err error
429429
}
430430

431-
func createTempFile(t *testing.T, name string, size int64) (*File, []byte) {
432-
f, err := CreateTemp(t.TempDir(), name)
431+
func createTempFile(tb testing.TB, name string, size int64) (*File, []byte) {
432+
f, err := CreateTemp(tb.TempDir(), name)
433433
if err != nil {
434-
t.Fatalf("failed to create temporary file: %v", err)
434+
tb.Fatalf("failed to create temporary file: %v", err)
435435
}
436-
t.Cleanup(func() {
436+
tb.Cleanup(func() {
437437
f.Close()
438438
})
439439

440440
randSeed := time.Now().Unix()
441-
t.Logf("random data seed: %d\n", randSeed)
441+
tb.Logf("random data seed: %d\n", randSeed)
442442
prng := rand.New(rand.NewSource(randSeed))
443443
data := make([]byte, size)
444444
prng.Read(data)
445445
if _, err := f.Write(data); err != nil {
446-
t.Fatalf("failed to create and feed the file: %v", err)
446+
tb.Fatalf("failed to create and feed the file: %v", err)
447447
}
448448
if err := f.Sync(); err != nil {
449-
t.Fatalf("failed to save the file: %v", err)
449+
tb.Fatalf("failed to save the file: %v", err)
450450
}
451451
if _, err := f.Seek(0, io.SeekStart); err != nil {
452-
t.Fatalf("failed to rewind the file: %v", err)
452+
tb.Fatalf("failed to rewind the file: %v", err)
453453
}
454454

455455
return f, data

0 commit comments

Comments
 (0)