GoとCはどちらもシステムコールを直接含みます(技術的には、Cはスタブを呼び出します)。
技術的には、書き込みはシステムコールと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)
}
}
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回に数回実行):
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、1000000回に数回実行)
私のベンチマークによると、純粋なCシステムコールは、最新リリース(go1.11)の純粋なGoシステムコールよりも15.82%高速です。
私は何を取りこぼしたか?理由とそれらを最適化する方法は何でしょうか?
その理由は、CとGo(Goがサポートする一般的なプラットフォーム(Linux、* BSD、Windowsなど))は両方ともマシンコードにコンパイルされますが、GoネイティブコードはCとはまったく異なる環境で実行されるためです。
Cとの2つの主な違いは次のとおりです。
したがって、Goコードがシステムコールを作成したい場合、かなり多くのことが起こるはずです。
P
sは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側で複数のシステムコールが実行されます。