Go语言 执行系统调用与C系统调用

omvjsjqw  于 2022-12-20  发布在  Go
关注(0)|答案(1)|浏览(202)

Go语言和C语言都直接涉及系统调用(从技术上讲,C语言会调用存根)。
从技术上讲,write既是一个系统调用,也是一个C函数(至少在许多系统上是这样)。然而,C函数只是一个调用系统调用的存根。Go语言并不调用这个存根,而是直接调用系统调用,这意味着这里不涉及C
Differences between C write call and Go syscall.Write开始
我的基准测试显示,纯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)
    }
}

丙:

#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);
}

340个不同的运行,每个C运行包含500000次执行,每个Go运行包含b. N次执行(大部分是500000次,100000次中有几次执行):

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次,100000次中很少执行)

gwo2fgha

gwo2fgha1#

我的基准测试显示,纯C系统调用比最新版本(go1.11)中的纯Go系统调用快15.82%。
我错过了什么?什么可能是原因?如何优化它们?
原因在于,虽然C和Go语言(在Go语言支持的典型平台上,如Linux、* BSD或Windows)都被编译成机器码,但Go语言的原生代码运行在与C语言完全不同的环境中。
与C的两个主要区别是:

  • Go语言的代码运行在goroutine环境中,goroutine由Go语言运行时在不同的操作系统线程上自由调度。
  • Goroutine使用它们自己的(可增长的和 * 可重新分配的)* 轻量级栈,这些栈与操作系统提供的C代码使用的栈无关。

因此,当Go语言代码想要进行系统调用时,需要做很多事情:
1.即将进入系统调用的goroutine必须被"固定"到它当前运行的操作系统线程上。
1.必须将执行切换为使用操作系统提供的C堆栈。

  1. Go语言运行时的调度器已经做好了必要的准备。
  2. goroutine进入系统调用。
    1.在退出后,goroutine的执行必须被恢复,这本身是一个相对复杂的过程,如果goroutine在系统调用中的时间太长,调度器从goroutine下移除所谓的"处理器",产生另一个操作系统线程,并使该处理器运行另一个goroutine,这可能会额外地受到阻碍("处理器",或P是在操作系统线程上运行goroutine的东西)。
  • 更新 * 以回答OP的评论

<...>因此,没有办法优化,如果我进行大量的IO调用,我必须忍受,不是吗?
这在很大程度上取决于您所追求的"大规模I/O"的性质。
如果您的示例(使用socketpair(2))不是简单的,那么就没有理由直接使用系统调用:socketpair(2)返回的FD是"可轮询的",因此Go语言运行时可以使用其原生的"netpoller"机制对它们执行I/O操作。下面是我的一个项目中的代码,它正确地" Package "了socketpair(2)生成的FD,使它们可以作为"常规"套接字使用(由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代码-链接或挂接为外部进程-这将以某种方式"批处理"它们,以便每次调用该C代码都将导致C端执行几次系统调用)。
See also.

相关问题