libfiber vs libco vs libmill vs Go: Performance Benchmark Comparison

According to the echo-IO benchmark in the iqiyi/libfiber repository, libfiber achieves up to 231,722 transactions per second with 100 concurrent fibers, significantly outperforming libco (147,310 TPS), libmill (201,171 TPS), and the Go runtime (154,504 TPS) while maintaining full POSIX compatibility.

The iqiyi/libfiber repository provides a production-grade native coroutine library for C/C++ designed for high-concurrency network programming. This libfiber vs libco vs libmill vs golang performance benchmark analysis examines the standardized echo-IO test results stored in doc/benchmark.txt to reveal how stackful coroutines compare against Go's preemptive goroutine scheduler in raw throughput metrics.

Echo-IO Benchmark Results

The benchmark executes 10 million echo operations between client-server pairs, measuring transactions per second (TPS) across varying concurrency levels. The raw data is located in doc/benchmark.txt at lines 27-68.

Library 100 Fibers (TPS) 200 Fibers (TPS) 500 Fibers (TPS) 1,000 Fibers (TPS)
libfiber 231,722 214,434 196,511 169,750
libco 147,310 135,781 128,509 106,070
libmill 201,171 154,527 148,741 132,678
Go runtime 154,504 160,226 140,219 123,268

At 1,000 concurrent fibers, libfiber sustains approximately 169,750 TPS, compared to libco's 106,070 TPS and libmill's 132,678 TPS. The gap widens at lower concurrency levels, where libfiber's optimized event loop and shared-stack architecture deliver superior cache locality.

Architectural Comparison

The performance gaps stem from fundamental design choices in stack management, scheduling models, and I/O integration.

libfiber: Native Coroutines with Shared-Stack Mode

libfiber implements lightweight stackful coroutines in C/C++ with an optional shared-stack mode that reduces memory pressure. As implemented in cpp/src/fiber.cpp, the library uses a single-threaded cooperative scheduler that can run independent schedulers per thread. It hooks standard POSIX I/O functions—including read, write, connect, and sleep—via the detours mechanism defined in cpp/src/detours/detours.h, allowing legacy blocking code to execute as non-blocking coroutines without modification.

libco: Pure C Stackful Coroutines

Tencent's libco provides stackful coroutines where each fiber owns a private stack. It uses a cooperative scheduler with per-thread queues and requires explicit co_resume calls. Unlike libfiber, libco does not provide built-in system-call hooking; developers must manually integrate external event loops (epoll/kqueue) and replace blocking calls manually.

libmill: Sustrik's Coroutine Primitives

libmill offers stackful coroutines with a small C runtime that supports multi-threaded mode via ml_thread. Instead of hooking system calls, it provides distinct non-blocking I/O primitives (ml_socket, ml_accept, etc.) that internally use epoll/kqueue. This requires code changes to use the library-specific API surface rather than standard POSIX sockets.

Go Runtime: Preemptive M-P-G Scheduler

The Go runtime implements a preemptive scheduler using the M-P-G model (OS threads M, processors P, goroutines G). Goroutine stacks grow dynamically, and the scheduler can migrate goroutines across threads for true multi-core parallelism. Go's built-in netpoller abstracts epoll/kqueue/IOCP, providing async semantics without external hooking.

Why libfiber Leads in Throughput

Three technical advantages explain libfiber's performance lead in the doc/benchmark.txt metrics:

  • Automatic System-Call Hooking: By intercepting blocking calls in-place through the detours mechanism, libfiber eliminates wrapper API overhead. The coroutine yields precisely when the OS would block, reducing context-switch latency compared to libmill's explicit API or libco's manual integration.
  • Shared-Stack Memory Efficiency: The shared-stack mode minimizes memory consumption, allowing more fibers to remain cache-resident. This improves context-switch speed relative to libco's fixed private stacks and Go's dynamically growing stacks.
  • Optimized Event Loop: The library selects the most efficient event mechanism per platform—including io_uring on Linux, epoll, kqueue, and IOCP—reducing latency compared to the generic loops used by libco and libmill.

Implementation Examples

The repository's samples/ and benchmark/ directories demonstrate equivalent echo-server implementations across all four frameworks.

libfiber Echo Server (C)

#include "fiber/lib_fiber.h"
#include "patch.h"

static void echo_fiber(ACL_FIBER *fb, void *ctx) {
    SOCKET client = *(SOCKET *)ctx;
    char buf[4096];
    int n;

    while ((n = acl_fiber_recv(client, buf, sizeof(buf), 0)) > 0) {
        acl_fiber_send(client, buf, n, 0);
    }
    socket_close(client);
    free(ctx);
}

int main(void) {
    const char *ip = "127.0.0.1";
    int port = 9000;
    SOCKET lfd = socket_listen(ip, port);
    socket_init();
    
    acl_fiber_create([](ACL_FIBER*,void*){ /* accept loop */ }, NULL, 128000);
    acl_fiber_schedule_with(FIBER_EVENT_KERNEL);
    
    socket_end();
}

This example from samples/cpp/server/main.cpp leverages acl_fiber_schedule_with to enter the event loop and uses hooked socket operations.

libco Echo Server (C)

#include "co.h"

static void *co_echo(void *arg) {
    int client = *(int *)arg;
    char buf[1024];
    int n;
    while ((n = recv(client, buf, sizeof(buf), 0)) > 0) {
        send(client, buf, n, 0);
    }
    close(client);
    free(arg);
    return NULL;
}

int main() {
    /* listen socket setup omitted */
    while (1) {
        int *c = malloc(sizeof(int));
        *c = accept(lfd, NULL, NULL);
        co_create(128*1024, co_echo, c);
    }
    co_eventloop(co_get_epoll_handle(), -1);
}

Adapted from the benchmark/libco directory, this requires manual event loop integration via co_eventloop.

libmill Echo Server (C)

#include <ml.h>

static void echo(void *arg) {
    ml_handle h = *(ml_handle *)arg;
    char buf[1024];
    int n;
    while ((n = ml_read(h, buf, sizeof(buf), 0)) > 0)
        ml_write(h, buf, n, 0);
    ml_close(h);
    free(arg);
}

int main() {
    ml_handle l = ml_socket();
    ml_bind(l, "0.0.0.0", "9000");
    ml_listen(l, 128);
    while (1) {
        ml_handle *c = malloc(sizeof(ml_handle));
        *c = ml_accept(l, NULL);
        ml_go(echo, c);
    }
}

This benchmark/libmill example uses library-specific primitives (ml_socket, ml_go) rather than POSIX sockets.

Go Echo Server

package main

import (
    "io"
    "log"
    "net"
)

func handle(conn net.Conn) {
    defer conn.Close()
    io.Copy(conn, conn)
}

func main() {
    ln, err := net.Listen("tcp", ":9000")
    if err != nil { log.Fatal(err) }
    for {
        c, err := ln.Accept()
        if err != nil { continue }
        go handle(c)
    }
}

The Go implementation uses the standard library's netpoller, which provides async I/O without explicit hooking.

Summary

  • libfiber achieves the highest throughput (231,722 TPS at 100 fibers) by combining shared-stack coroutines with automatic POSIX hooking and platform-optimized event loops.
  • libco trades performance (147,310 TPS) for simplicity, requiring manual event loop integration and lacking system-call interception.
  • libmill delivers moderate performance (201,171 TPS) but forces migration to a non-standard API surface.
  • Go provides the best multi-core parallelism through preemptive scheduling but achieves lower single-threaded throughput (154,504 TPS) compared to libfiber's cooperative model.
  • The benchmark data in doc/benchmark.txt confirms that libfiber's hooking mechanism and memory efficiency provide measurable advantages for high-concurrency C/C++ applications.

Frequently Asked Questions

What benchmark test was used to compare these coroutine libraries?

The comparison used an echo-IO benchmark executing 10 million echo operations between client-server pairs, measuring transactions per second (TPS) across 100, 200, 500, and 1,000 concurrent fibers. The test configuration and raw results are documented in doc/benchmark.txt at lines 27-68.

Why does libfiber outperform Go's goroutines in this benchmark?

libfiber uses a cooperative scheduler with automatic system-call hooking that eliminates context-switch overhead, while Go's preemptive M-P-G scheduler incurs synchronization costs for cross-thread goroutine migration. Additionally, libfiber's shared-stack mode reduces memory pressure compared to Go's dynamically growing stacks, improving cache locality during the echo-IO test.

Can existing C/C++ code use libfiber without modification?

Yes. According to the README.md and implementation in cpp/src/detours/, libfiber hooks standard POSIX APIs including read, write, connect, sleep, and DNS functions. This allows existing blocking code to run as non-blocking coroutines without changing the application logic or switching to library-specific I/O primitives like those required by libmill.

How does libfiber's shared-stack mode improve performance?

The shared-stack mode, implemented in cpp/src/fiber.cpp, allows multiple fibers to share a single stack region rather than allocating fixed private stacks like libco. This reduces memory consumption significantly, allowing more fibers to fit in CPU cache and decreasing context-switch latency, which directly contributes to the higher TPS observed in the 1,000-fiber benchmark (169,750 TPS vs. libco's 106,070 TPS).

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →