簡體   English   中英

Go 系統調用與 C 系統調用

[英]Go syscall v.s. C system call

Go 和 C 都直接涉及系統調用(從技術上講,C 將調用存根)。

從技術上講,write 既是系統調用又是 C 函數(至少在許多系統上是這樣)。 然而,C 函數只是一個調用系統調用的存根。 Go 沒有調用這個 stub,它直接調用系統調用,也就是說這里不涉及 C

來自C write call 和 Go syscall.Write 之間的差異

我的基准測試顯示,在最新版本 (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!\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運行包含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)
}

在此處輸入圖片說明

上圖顯示,100次不同的運行,每次C運行包含500000次執行,每次Go運行包含bN次執行(多為500000次,少數執行1000000次)

我的基准測試顯示,在最新版本 (go1.11) 中,純 C 系統調用比純 Go 系統調用快 15.82%。

我錯過了什么? 可能是什么原因以及如何優化它們?

原因是,雖然 C 和 Go(在 Go 支持的典型平台上——例如 Linux 或 *BSD 或 Windows)都被編譯為機器代碼,但 Go-native 代碼在與 C 完全不同的環境中運行。

與 C 的兩個主要區別是:

  • Go 代碼在所謂的 goroutine 的上下文中運行,Go 運行時在不同的 OS 線程上自由調度這些 goroutine。
  • Goroutines 使用它們自己的(可增長和可重新分配的)輕量級堆棧,這些堆棧與操作系統提供的堆棧 C 代碼使用無關。

因此,當 Go 代碼想要進行系統調用時,應該會發生很多事情:

  1. 即將進入系統調用的 goroutine 必須“固定”到它當前運行的操作系統線程。
  2. 必須切換執行以使用操作系統提供的 C 堆棧。
  3. 在 Go 運行時的調度程序中進行了必要的准備。
  4. goroutine 進入系統調用。
  5. 退出時必須恢復 goroutine 的執行,這本身是一個相對復雜的過程,如果 goroutine系統調用中的時間過長並且調度程序從該 goroutine 下刪除了所謂的“處理器”,則可能會受到額外的阻礙,產生另一個 OS 線程並使該處理器運行另一個 goroutine(“處理器”,或P是在 OS 線程上運行 goroutine 的東西)。

更新以回答 OP 的評論

<...> 因此沒有辦法優化,如果我進行大量 IO 調用,我必須忍受,不是嗎?

這在很大程度上取決於您所追求的“大規模 I/O”的性質。

如果您的示例(使用socketpair(2) )不是玩具,則根本沒有理由直接使用系統調用: socketpair(2)返回的socketpair(2)是“可輪詢的”,因此 Go 運行時可能會使用其原生的“netpoller”機制來對它們執行 I/O。 這是我的一個項目中的一個工作代碼,它正確地“包裝”了socketpair(2)生成的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, 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 端完成多個系統調用)。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM