Перейти системный вызов против. C системный вызов - PullRequest
0 голосов
/ 12 сентября 2018

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 раз):

enter image description here

T-Проверка на 2 независимых значения: значение t составляет -22,45426.Значение р <.00001.Результат значим при p <.05. </p>

enter image description here

Калькулятор T-теста для 2 зависимых средних: значение t равно 15,902782.Значение р <0,00001.Результат значим при p ≤ 0,05. </p>

enter image description here


Обновление: я обработал предложение в ответах и ​​написал другой тест, он показываетПредлагаемый подход значительно снижает производительность массовых вызовов ввода-вывода, его производительность близка к 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 содержит bN выполнений (в основном 500000, несколько раз выполнено в 1000000 раз)

1 Ответ

0 голосов
/ 12 сентября 2018

Мой тест показывает, что системный вызов на чистом C на 15,82% быстрее системного вызова на чистом Go в последней версии (go1.11).

Что я пропустил?В чем может быть причина и как их оптимизировать?

Причина в том, что хотя C и Go (на типичной платформе Go поддерживает - например, Linux или * BSD или Windows) компилируются вмашинный код, код Go-native выполняется в среде, совершенно отличной от среды C.

Два главных отличия от C:

  • Код Go запускается в контекстеназываемые goroutines, которые свободно планируются средой выполнения Go в различных потоках ОС.
  • Goroutines используют свои собственные (растущие и reallocatable) облегченные стеки, которые не имеют ничего общего с поставляемым ОС стеком Cиспользуется код.

Итак, когда код Go хочет выполнить системный вызов, должно произойти довольно много:

  1. Программа, которая собирается ввести системный вызов, должна быть "прикреплен "к потоку ОС, в котором он в данный момент работает.
  2. Выполнение должно быть переключено на использование поставляемого ОС стека C.
  3. Необходимая подготовка в планировщике среды выполнения Go are made.
  4. В системный вызов входит goroutine.
  5. После выхода выполнение goroutine должно быть возобновлено, что само по себе является относительно сложным процессом, который может быть дополнительно затруднен, если goroutine был в системный вызов слишком долго, и планировщик удалил так называемый «процессор» из-под этой процедуры, породил другой поток ОС и заставил этот процессор запустить другую процедуру («процессоры», или P с).штуковины, которые запускают программы в потоках ОС).

Обновление , чтобы ответить на комментарий ОП

<…> Таким образом, нет никакого способаоптимизировать, и я должен страдать от того, что если я делаю массивные вызовы ввода-вывода, не так ли?

Это сильно зависит от характера "массивного ввода-вывода", который вы ищете.

Если ваш пример (с socketpair(2)) не является игрушкой, просто нет причин использовать системные вызовы напрямую: FD, возвращаемые socketpair(2), являются "запрашиваемыми", и, следовательно, среда выполнения Go может использовать свой собственный механизм "netpoller"наФорма ввода / вывода на них.Вот рабочий код из одного из моих проектов, который правильно «оборачивает» FD, созданные в 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, err
}

func fdToFileConn(fd int) (net.Conn, error) {
       f := os.NewFile(uintptr(fd), "")
       defer f.Close()
       return net.FileConn(f)
}

Если вы говорите о каком-либо другом типе ввода / вывода, ответ таков: да, системные вызовы не очень дешевы, и если вы должны сделать много из них, есть способы обойтиих стоимость (такая как выгрузка в некоторый код C - связанный или подключенный как внешний процесс), который каким-то образом пакетирует их так, что каждый вызов этого кода C приведет к нескольким системным вызовам, выполняемым стороной C).

...