进行系统调用 v.s。 C 系统调用
Go syscall v.s. C system call
Go,C都直接涉及系统调用(技术上,C会调用存根)。
Technically, write is both a system call and a C function (at least on many systems). However, the C function is just a stub which invokes the system call. Go does not call this stub, it invokes the system call directly, which means that C is not involved here
From
我的基准测试显示,在最新版本 (go1.11) 中,纯 C 系统调用比纯 Go 系统调用快 15.82%。
我错过了什么?可能是什么原因以及如何优化它们?
基准:
开始:
package main_test
import (
"syscall"
"testing"
)
func writeAll(fd int, buf []byte) error {
for len(buf) > 0 {
n, err := syscall.Write(fd, buf)
if n < 0 {
return err
}
buf = buf[n:]
}
return nil
}
func BenchmarkReadWriteGoCalls(b *testing.B) {
fds, _ := syscall.Socketpair(syscall.AF_UNIX, syscall.SOCK_STREAM, 0)
message := "hello, world!"
buffer := make([]byte, 13)
for i := 0; i < b.N; i++ {
writeAll(fds[0], []byte(message))
syscall.Read(fds[1], buffer)
}
}
C:
#include <time.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/socket.h>
int write_all(int fd, void* buffer, size_t length) {
while (length > 0) {
int written = write(fd, buffer, length);
if (written < 0)
return -1;
length -= written;
buffer += written;
}
return length;
}
int read_call(int fd, void *buffer, size_t length) {
return read(fd, buffer, length);
}
struct timespec timer_start(){
struct timespec start_time;
clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &start_time);
return start_time;
}
long timer_end(struct timespec start_time){
struct timespec end_time;
clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &end_time);
long diffInNanos = (end_time.tv_sec - start_time.tv_sec) * (long)1e9 + (end_time.tv_nsec - start_time.tv_nsec);
return diffInNanos;
}
int main() {
int i = 0;
int N = 500000;
int fds[2];
char message[14] = "hello, world![=12=]";
char buffer[14] = {0};
socketpair(AF_UNIX, SOCK_STREAM, 0, fds);
struct timespec vartime = timer_start();
for(i = 0; i < N; i++) {
write_all(fds[0], message, sizeof(message));
read_call(fds[1], buffer, 14);
}
long time_elapsed_nanos = timer_end(vartime);
printf("BenchmarkReadWritePureCCalls\t%d\t%.2ld ns/op\n", N, time_elapsed_nanos/N);
}
340个不同的运行,每个C运行包含500000次执行,每个Go运行包含b.N次执行(大部分是500000次,1000000次执行的次数很少) :
2 个独立均值的 T 检验:t 值为 -22.45426。 p 值 < .00001。结果在 p < .05 时显着。
2 个相关均值的 T 检验计算器:t 的值为 15.902782。 p 的值 < 0.00001。结果在 p ≤ 0.05 时显着。
更新:我管理了答案中的提议并编写了另一个基准测试,它表明提议的方法显着降低了大规模 I/O 调用的性能,其性能接近 CGO 调用。
基准:
func BenchmarkReadWriteNetCalls(b *testing.B) {
cs, _ := socketpair()
message := "hello, world!"
buffer := make([]byte, 13)
for i := 0; i < b.N; i++ {
cs[0].Write([]byte(message))
cs[1].Read(buffer)
}
}
func socketpair() (conns [2]net.Conn, err error) {
fds, err := syscall.Socketpair(syscall.AF_LOCAL, syscall.SOCK_STREAM, 0)
if err != nil {
return
}
conns[0], err = fdToFileConn(fds[0])
if err != nil {
return
}
conns[1], err = fdToFileConn(fds[1])
if err != nil {
conns[0].Close()
return
}
return
}
func fdToFileConn(fd int) (net.Conn, error) {
f := os.NewFile(uintptr(fd), "")
defer f.Close()
return net.FileConn(f)
}
上图显示,100个不同的运行,每个C运行包含500000次执行,每个Go运行包含b.N次执行(主要是500000次,少数执行 1000000 次)
My benchmark shows, pure C system call is 15.82% faster than pure Go system call in the latest release (go1.11).
What did I miss? What could be a reason and how to optimize them?
原因是,虽然 C 和 Go(在 Go 支持的典型平台上——例如 Linux 或 *BSD 或 Windows)都被编译为机器代码,但 Go-native 代码运行s 在与 C 完全不同的环境中。
与 C 的两个主要区别是:
- Go 代码 运行 在所谓的 goroutines 的上下文中,这些 goroutines 由 Go 运行time 在不同的 OS 线程上自由调度。
- Goroutines 使用它们自己的(可增长和 可重新分配) 轻量级堆栈,与 OS 提供的堆栈 C 代码使用无关。
所以,当 Go 代码想要进行系统调用时,应该发生很多事情:
- 即将进入系统调用的 goroutine 必须 "pinned" 到当前 运行 正在运行的 OS 线程。
- 必须将执行切换为使用 OS 提供的 C 堆栈。
- Go 运行time 调度程序中的必要准备工作已完成。
- 协程进入系统调用
- 退出时必须恢复 goroutine 的执行,这本身就是一个相对复杂的过程,如果 goroutine 在 在 系统调用中的时间过长,则可能会受到额外的阻碍并且调度程序从该 goroutine 下删除了所谓的 "processor",生成了另一个 OS 线程并使该处理器 运行 成为另一个 goroutine("processors",或
P
s 是 运行 goroutines 在 OS 线程上的东西)。
更新 以回答 OP 的评论
<…> Thus there is no way to optimize and I must suffer that if I make massive IO calls, mustn't I?
这在很大程度上取决于您所追求的 "massive I/O" 的性质。
如果您的示例(带有 socketpair(2)
)不是玩具,则根本没有理由直接使用系统调用:socketpair(2)
返回的 FD 是 "pollable",因此 Go 运行time 可以使用其原生的 "netpoller" 机制对它们执行 I/O。这是我的一个项目的工作代码,它正确地 "wraps" 由 socketpair(2)
生成的 FD,因此它们可以用作 "regular" 套接字(由 net
标准的函数生成包裹):
func socketpair() (net.Conn, net.Conn, error) {
fds, err := syscall.Socketpair(syscall.AF_LOCAL, syscall.SOCK_STREAM, 0)
if err != nil {
return nil, nil, err
}
c1, err := fdToFileConn(fds[0])
if err != nil {
return nil, nil, err
}
c2, err := fdToFileConn(fds[1])
if err != nil {
c1.Close()
return nil, nil, err
}
return c1, c2, nil
}
func fdToFileConn(fd int) (net.Conn, error) {
f := os.NewFile(uintptr(fd), "")
defer f.Close()
return net.FileConn(f)
}
如果你在谈论其他类型的 I/O,答案是肯定的,系统调用并不便宜,如果你 必须 做很多, 有一些方法可以解决它们的成本(例如卸载到一些 C 代码——链接或连接为外部进程——这将以某种方式 batch 它们以便每次调用该 C代码将导致 C 端完成多个系统调用。
Go,C都直接涉及系统调用(技术上,C会调用存根)。
Technically, write is both a system call and a C function (at least on many systems). However, the C function is just a stub which invokes the system call. Go does not call this stub, it invokes the system call directly, which means that C is not involved here
From
我的基准测试显示,在最新版本 (go1.11) 中,纯 C 系统调用比纯 Go 系统调用快 15.82%。
我错过了什么?可能是什么原因以及如何优化它们?
基准:
开始:
package main_test
import (
"syscall"
"testing"
)
func writeAll(fd int, buf []byte) error {
for len(buf) > 0 {
n, err := syscall.Write(fd, buf)
if n < 0 {
return err
}
buf = buf[n:]
}
return nil
}
func BenchmarkReadWriteGoCalls(b *testing.B) {
fds, _ := syscall.Socketpair(syscall.AF_UNIX, syscall.SOCK_STREAM, 0)
message := "hello, world!"
buffer := make([]byte, 13)
for i := 0; i < b.N; i++ {
writeAll(fds[0], []byte(message))
syscall.Read(fds[1], buffer)
}
}
C:
#include <time.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/socket.h>
int write_all(int fd, void* buffer, size_t length) {
while (length > 0) {
int written = write(fd, buffer, length);
if (written < 0)
return -1;
length -= written;
buffer += written;
}
return length;
}
int read_call(int fd, void *buffer, size_t length) {
return read(fd, buffer, length);
}
struct timespec timer_start(){
struct timespec start_time;
clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &start_time);
return start_time;
}
long timer_end(struct timespec start_time){
struct timespec end_time;
clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &end_time);
long diffInNanos = (end_time.tv_sec - start_time.tv_sec) * (long)1e9 + (end_time.tv_nsec - start_time.tv_nsec);
return diffInNanos;
}
int main() {
int i = 0;
int N = 500000;
int fds[2];
char message[14] = "hello, world![=12=]";
char buffer[14] = {0};
socketpair(AF_UNIX, SOCK_STREAM, 0, fds);
struct timespec vartime = timer_start();
for(i = 0; i < N; i++) {
write_all(fds[0], message, sizeof(message));
read_call(fds[1], buffer, 14);
}
long time_elapsed_nanos = timer_end(vartime);
printf("BenchmarkReadWritePureCCalls\t%d\t%.2ld ns/op\n", N, time_elapsed_nanos/N);
}
340个不同的运行,每个C运行包含500000次执行,每个Go运行包含b.N次执行(大部分是500000次,1000000次执行的次数很少) :
2 个独立均值的 T 检验:t 值为 -22.45426。 p 值 < .00001。结果在 p < .05 时显着。
2 个相关均值的 T 检验计算器:t 的值为 15.902782。 p 的值 < 0.00001。结果在 p ≤ 0.05 时显着。
更新:我管理了答案中的提议并编写了另一个基准测试,它表明提议的方法显着降低了大规模 I/O 调用的性能,其性能接近 CGO 调用。
基准:
func BenchmarkReadWriteNetCalls(b *testing.B) {
cs, _ := socketpair()
message := "hello, world!"
buffer := make([]byte, 13)
for i := 0; i < b.N; i++ {
cs[0].Write([]byte(message))
cs[1].Read(buffer)
}
}
func socketpair() (conns [2]net.Conn, err error) {
fds, err := syscall.Socketpair(syscall.AF_LOCAL, syscall.SOCK_STREAM, 0)
if err != nil {
return
}
conns[0], err = fdToFileConn(fds[0])
if err != nil {
return
}
conns[1], err = fdToFileConn(fds[1])
if err != nil {
conns[0].Close()
return
}
return
}
func fdToFileConn(fd int) (net.Conn, error) {
f := os.NewFile(uintptr(fd), "")
defer f.Close()
return net.FileConn(f)
}
上图显示,100个不同的运行,每个C运行包含500000次执行,每个Go运行包含b.N次执行(主要是500000次,少数执行 1000000 次)
My benchmark shows, pure C system call is 15.82% faster than pure Go system call in the latest release (go1.11).
What did I miss? What could be a reason and how to optimize them?
原因是,虽然 C 和 Go(在 Go 支持的典型平台上——例如 Linux 或 *BSD 或 Windows)都被编译为机器代码,但 Go-native 代码运行s 在与 C 完全不同的环境中。
与 C 的两个主要区别是:
- Go 代码 运行 在所谓的 goroutines 的上下文中,这些 goroutines 由 Go 运行time 在不同的 OS 线程上自由调度。
- Goroutines 使用它们自己的(可增长和 可重新分配) 轻量级堆栈,与 OS 提供的堆栈 C 代码使用无关。
所以,当 Go 代码想要进行系统调用时,应该发生很多事情:
- 即将进入系统调用的 goroutine 必须 "pinned" 到当前 运行 正在运行的 OS 线程。
- 必须将执行切换为使用 OS 提供的 C 堆栈。
- Go 运行time 调度程序中的必要准备工作已完成。
- 协程进入系统调用
- 退出时必须恢复 goroutine 的执行,这本身就是一个相对复杂的过程,如果 goroutine 在 在 系统调用中的时间过长,则可能会受到额外的阻碍并且调度程序从该 goroutine 下删除了所谓的 "processor",生成了另一个 OS 线程并使该处理器 运行 成为另一个 goroutine("processors",或
P
s 是 运行 goroutines 在 OS 线程上的东西)。
更新 以回答 OP 的评论
<…> Thus there is no way to optimize and I must suffer that if I make massive IO calls, mustn't I?
这在很大程度上取决于您所追求的 "massive I/O" 的性质。
如果您的示例(带有 socketpair(2)
)不是玩具,则根本没有理由直接使用系统调用:socketpair(2)
返回的 FD 是 "pollable",因此 Go 运行time 可以使用其原生的 "netpoller" 机制对它们执行 I/O。这是我的一个项目的工作代码,它正确地 "wraps" 由 socketpair(2)
生成的 FD,因此它们可以用作 "regular" 套接字(由 net
标准的函数生成包裹):
func socketpair() (net.Conn, net.Conn, error) {
fds, err := syscall.Socketpair(syscall.AF_LOCAL, syscall.SOCK_STREAM, 0)
if err != nil {
return nil, nil, err
}
c1, err := fdToFileConn(fds[0])
if err != nil {
return nil, nil, err
}
c2, err := fdToFileConn(fds[1])
if err != nil {
c1.Close()
return nil, nil, err
}
return c1, c2, nil
}
func fdToFileConn(fd int) (net.Conn, error) {
f := os.NewFile(uintptr(fd), "")
defer f.Close()
return net.FileConn(f)
}
如果你在谈论其他类型的 I/O,答案是肯定的,系统调用并不便宜,如果你 必须 做很多, 有一些方法可以解决它们的成本(例如卸载到一些 C 代码——链接或连接为外部进程——这将以某种方式 batch 它们以便每次调用该 C代码将导致 C 端完成多个系统调用。