Go и C оба задействуют системные вызовы напрямую (Технически, C вызовет заглушку).
Технически запись - это и системный вызов, и функция C (по крайней мере, во многих системах),Однако функция C - это просто заглушка, которая вызывает системный вызов.Go не вызывает эту заглушку, она вызывает системный вызов напрямую, что означает, что C здесь не задействован
From Различия между вызовом записи C и Go syscall.Write
Мой тест показывает, что системный вызов на чистом C на 15,82% быстрее системного вызова на чистом Go в последней версии (go1.11).
Что я пропустил?В чем может быть причина и как их оптимизировать?
Тесты:
Go:
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 раз):
T-Проверка на 2 независимых значения: значение t составляет -22,45426.Значение р <.00001.Результат значим при p <.05. </p>
Калькулятор T-теста для 2 зависимых средних: значение t равно 15,902782.Значение р <0,00001.Результат значим при p ≤ 0,05. </p>
Обновление: я обработал предложение в ответах и написал другой тест, он показываетПредлагаемый подход значительно снижает производительность массовых вызовов ввода-вывода, его производительность близка к 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 раз)