web-dev-qa-db-ja.com

システムコールv.sに移動します。 Cシステムコール

GoとCはどちらもシステムコールを直接含みます(技術的には、Cはスタブを呼び出します)。

技術的には、書き込みはシステムコールとC関数の両方です(少なくとも多くのシステムでは)。ただし、C関数は、システムコールを呼び出す単なるスタブです。 Goはこのスタブを呼び出さず、システムコールを直接呼び出します。つまり、ここではCは関与しません。

From C書き込み呼び出しと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)
    }
}

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の実行にはb.Nの実行が含まれます(ほとんどは500000、1000000回に数回実行):

enter image description here

2つの独立した平均のT検定:t値は-22.45426です。 p値は<.00001です。結果は、p <.05で有意です。

enter image description here

2つの従属平均のT検定計算機:tの値は15.902782です。 pの値は<0.00001です。結果は、p≤0.05で有意です。

enter image description here


更新:回答で提案を管理し、別のベンチマークを作成しました。提案されたアプローチは、大規模な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)
}

enter image description here

上の図は、100の異なる実行、各Cの実行には500000の実行が含まれ、各Goの実行にはb.Nの実行が含まれることを示しています(ほとんどが500000、1000000回に数回実行)

12
Jakob

私のベンチマークによると、純粋なCシステムコールは、最新リリース(go1.11)の純粋なGoシステムコールよりも15.82%高速です。

私は何を取りこぼしたか?理由とそれらを最適化する方法は何でしょうか?

その理由は、CとGo(Goがサポートする一般的なプラットフォーム(Linux、* BSD、Windowsなど))は両方ともマシンコードにコンパイルされますが、GoネイティブコードはCとはまったく異なる環境で実行されるためです。

Cとの2つの主な違いは次のとおりです。

  • Goコードは、さまざまなOSスレッドでGoランタイムによって自由にスケジュールされるいわゆるゴルーチンのコンテキストで実行されます。
  • Goroutinesは、独自の(growableおよびreallocatable)軽量スタックを使用します。これは、OSが提供するスタックCコードの使用とは関係ありません。

したがって、Goコードがシステムコールを作成したい場合、かなり多くのことが起こるはずです。

  1. システムコールを開始しようとしているゴルーチンは、現在実行されているOSスレッドに「固定」されている必要があります。
  2. OS提供のCスタックを使用するには、実行を切り替える必要があります。
  3. Goランタイムのスケジューラーで必要な準備が行われます。
  4. ゴルーチンがシステムコールに入ります。
  5. 終了時に、ゴルーチンの実行を再開する必要があります。これは、それ自体が比較的複雑なプロセスであり、ゴルーチンがin syscallが長すぎて、スケジューラがいわゆるを削除した場合、さらに妨げられる可能性があります。そのゴルーチンの下から「プロセッサ」が別のOSスレッドを生成し、そのプロセッサに別のゴルーチンを実行させました(「プロセッサ」、またはPsはOSスレッドでゴルーチンを実行するものです)。

更新 OPのコメントに答える

<…>したがって、最適化する方法はなく、大量のIO呼び出しを行うと、私はそうしませんか?

それはあなたが求めている「大規模なI/O」の性質に大きく依存します。

あなたの例(socketpair(2)を使用)がおもちゃではない場合、syscallを直接使用する理由はありません。socketpair(2)によって返されるFDは「ポーリング可能」であるため、Goランタイムはそれらに対してI/Oを実行するためのネイティブの「netpoller」機構。これは、socketpair(2)によって生成されたFDを適切に「ラップ」して、「通常の」ソケット(net標準パッケージの関数によって生成された)として使用できるようにする、私のプロジェクトの1つからの作業コードです。 )::

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

他の種類のI/Oについて話している場合、答えはそうです、システムコールはそれほど安くはありません。必須多くのI/Oを実行する場合、コストを回避する方法があります(たとえば、外部プロセスとしてリンクまたはフックされたCコードへのオフロードは、何らかの形でbatchであるため、そのCコードを呼び出すたびに、C側で複数のシステムコールが実行されます。

15
kostix