首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >去系统呼叫

去系统呼叫
EN

Stack Overflow用户
提问于 2018-09-12 14:00:57
回答 1查看 4.4K关注 0票数 15

Go和C都直接涉及系统调用(从技术上讲,C将调用存根)。

从技术上讲,写既是一个系统调用,也是一个C函数(至少在许多系统上是这样)。但是,C函数只是调用系统调用的存根。Go不调用这个存根,它直接调用系统调用,这意味着这里不涉及C。 来自Differences between C write call and Go syscall.Write

我的基准测试显示,在最新版本(go1.11)中,纯C系统调用比纯Go系统调用快15.82%。

我错过了什么?原因是什么,以及如何优化它们?

基准:

去:

代码语言:javascript
复制
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:

代码语言:javascript
复制
#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次,在1000000次中很少执行):

两个独立均值的t-检验:t-值为-22.45426.P-值为< .00001.在p< .05时有显着性差异。

T-测试计算器的两个相依均值:t的值是15.902782.P值< 0.00001。在p≤0.0 5时,结果显著。

更新:我在答案中管理了提案并编写了另一个基准,它显示了所提议的方法显着地降低了大规模I/O调用的性能,其性能接近CGO调用。

基准:

代码语言:javascript
复制
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次中执行了几次)

EN

回答 1

Stack Overflow用户

回答已采纳

发布于 2018-09-12 15:14:25

我的基准测试显示,在最新版本(go1.11)中,纯C系统调用比纯Go系统调用快15.82%。 我错过了什么?原因是什么,以及如何优化它们?

原因是,虽然C和Go (在典型平台Go上支持--例如Linux或*BSD或Windows)都被编译成机器代码,但Go本机代码却在一个与C完全不同的环境中运行。

C与C的两个主要区别是:

  • Go代码在所谓的goroutines上下文中运行,Go运行时可以在不同的OS线程上自由地调度这些内容。
  • Goroutines使用自己的(可扩展和可分配的)轻量级堆栈,这与操作系统提供的堆栈C代码的使用无关。

因此,当Go代码想要制作syscall时,应该会发生很多事情:

  1. 即将进入syscall的goroutine必须“固定”在当前运行的OS线程上。
  2. 必须切换执行以使用OS提供的C堆栈。
  3. 在Go运行时的调度程序中进行了必要的准备。
  4. 猩猩进入了syscall。
  5. 一旦退出,必须恢复执行goroutine,这本身就是一个相对复杂的进程,如果goroutine在syscall中停留太长时间,而调度程序将所谓的“处理器”从该goroutine下移除,生成另一个OS线程并使该处理器运行另一个goroutine (“处理器”,或者P是在OS线程上运行大猩猩的稀薄程序),这本身就会受到额外的阻碍。

更新以回答OP的评论

<…因此,没有优化的方法,我必须忍受,如果我进行大量的IO调用,不是吗?

这在很大程度上取决于你所追求的“大规模I/O”的性质。

如果您的示例(使用socketpair(2))不是玩具,那么根本就没有理由直接使用syscalls:socketpair(2)返回的FDs是"pollable“,因此Go运行时可以使用它的本机"netpoller”机器对它们执行I/O。下面是我的一个项目的工作代码,它正确地“包装”了socketpair(2)生成的FDs,这样它们就可以作为“常规”套接字使用(由来自net标准包的函数生成):

代码语言:javascript
复制
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端完成几个系统)。

票数 18
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/52297027

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档