The Effect of Pipe Capacity on Unix Pipeline Performance

DONG Yuxuan @ Feb 25, 2020 CST

Depending on how two processes exchange data, we need different pipe capacities to maximize performance.

Overview

Consider a simple Unix pipeline:

$ a | b

How large should we set the capacity of the pipe to maximize the performance? It depends on how the two processes exchange data.

We can abstract process a as a computation-writing cycle:

a

Process b can be abstracted to a reading-computation cycle:

b

Based on the above abstraction, there’re three patterns that the two processes could exchange data in.

Pattern 1: One-to-one Exchange

One-to-one means one reading phase depends on one writing phase.

In this pattern, a reading phase of b only needs data written by one writing phase of a, like the following:

pat1

This is the most common pattern, for example, most Unix filters process data line by line.

If the two processes exchange data in this pattern, a small pipe doesn’t cause unnecessary blockings.

If b is faster, a will not be blocked by the capacity. If a is faster, b will not be blocked by the capacity. Thus the slower process will never be blocked by the capacity. The performance of the pipeline is its theoretical optimum value.

Pattern 2: One-to-many Exchange

One-to-many means one reading phase depends on many writing phases.

In this pattern, a reading phase of b needs data written by two or more writing phases of a, like the following:

pat2

This pattern could happen. For example, process a prints texts line by line, but process b needs at least two lines to compute.

In a one-to-many exchange, if process a is not faster than b, a small pipe doesn’t cause unnecessary blockings; if process a is faster than b, making the pipe large enough to store the data a reading phase wants to read can maximize the performance of the pipeline.

In the above example, this means we could set the pipe large enough to contain two text lines to maximize the performance if a is faster.

Since if process a is not the faster one, we could maximize the performance by ensuring writing phases will not be blocked. Whatever the capacity is, writing phases won’t be blocked because the faster process b is always ready to read.

If process a is faster, to maximize the performance, we should ensure reading phases in b don’t wait for computation in a. If the pipe can’t store all the data a reading phase needs, the waiting can happen. Because when b is in a slow computation, the writing phase in a could block the pipe, thus the next computation phase in a is blocked. When b finished the computation and needs to read the data, it must wait for the computation in a.

Pattern 3: Many-to-one Exchange

Many-to-one means many reading phases depend on one writing phase.

In this pattern, multiple reading phases of b need data written by one writing phase of a, like the following:

pat3

In this pattern, if process a is not slower than b, a small pipe doesn’t cause unnecessary blockings; if process a is slower than b, making the pipe large enough to store data which a writing phase wants to write can maximize the performance.

Since in this pattern if a is not the slower one, every time b needs data, it won’t be blocked. There’re no unnecessary blockings in the slower process.

If a is slower in this pattern, the writing phase can be blocked if the pipe is not large enough. If the pipe is large enough to not block a writing phase, it won’t block any other writing phases. Because a is slower, when it enters the next writing phase, b has already popped all the data in the pipe. Thus the slower process a has no unnecessary blockings.

Appendix: How to Set the Capacity of a Pipe

In Linux with kernel version 2.6.35 upwards, the capacity of a pipe can be set by the fcntl function.

/* The C Programming Language */

#include <unistd.h>
#include <fcntl.h>

...

fcntl(fd, F_SETPIPE_SZ, size)

However, F_SETPIPE_SZ has no Python binding. We must use its numeric value 0x407.

# Python3
# The snippet only works in Linux with kernel version 2.6.35 upwards

from fcntl import fcntl

...

fnctl(fd, 0x407, size)