Jak*_*kob 11 c system-calls go
Go和C都直接涉及系统调用(技术上,C将调用存根).
从技术上讲,write既是系统调用又是C函数(至少在许多系统上).但是,C函数只是一个调用系统调用的存根.Go不会调用此存根,它会直接调用系统调用,这意味着此处不涉及C.
我的基准测试显示,纯C系统调用比最新版本(go1.11)中的纯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)
}
}
Run Code Online (Sandbox Code Playgroud)
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!\0";
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);
}
Run Code Online (Sandbox Code Playgroud)
340个不同的运行,每个C运行包含500000个执行,每个Go运行包含bN执行(大多数为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)
}
Run Code Online (Sandbox Code Playgroud)
上图显示,100个不同的运行,每个C运行包含500000个执行,每个Go运行包含bN执行(大多数500000,几次执行1000000次)
kos*_*tix 15
我的基准测试显示,纯C系统调用比最新版本(go1.11)中的纯Go系统调用快15.82%.
我错过了什么?可能是什么原因以及如何优化它们?
原因是虽然C和Go(在典型的Go平台上支持 - 例如Linux或*BSD或Windows)都被编译成机器代码,但Go-native代码在与C完全不同的环境中运行.
与C的两个主要区别是:
因此,当Go代码想要进行系统调用时,应该发生很多事情:
Ps是在OS线程上运行goroutine的东西).更新以回答OP的评论
<...>因此没有办法进行优化,如果我进行大量IO调用,我必须忍受这种情况,不是吗?
它在很大程度上取决于你所追求的"大规模I/O"的性质.
如果您的示例(with socketpair(2))不是玩具,则根本没有理由直接使用系统调用:返回的socketpair(2)FD是"可轮询的",因此Go运行时可以使用其本机"netpoller"机制对它们执行I/O. 这是我的一个项目的工作代码,它正确地"包装"生成的FD,socketpair(2)以便它们可以用作"常规"套接字(由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, err
}
func fdToFileConn(fd int) (net.Conn, error) {
f := os.NewFile(uintptr(fd), "")
defer f.Close()
return net.FileConn(f)
}
Run Code Online (Sandbox Code Playgroud)
如果你正在谈论其他类型的I/O,答案是肯定的,系统调用并不是很便宜,如果你必须做很多,有办法解决它们的成本(比如卸载到一些C代码) - 作为外部进程链接或连接 - 这将以某种方式批处理它们,以便每次调用该C代码将导致C方完成多个系统调用).