pipe and redirection speed, `pv` and UUOC

Sep 04 2020

I was testing different methods to produce random garbage and comparing their speed by piping output to pv, as in:

$ cmd | pv -s "$size" -S > /dev/null

I also wanted a "baseline reference", so I measured the the fastest "generator", cat, with the fastest source, /dev/zero:

$ cat /dev/zero | pv -s 100G -S > /dev/null
 100GiB 0:00:33 [2,98GiB/s] [=============================>] 100%   

3GB/s, that's pretty impressive, specially compared to ~70MB I get from /dev/urandom.

But hey, for the special case of /dev/zero I don't need cat! Just for the kicks I removed this textbook UUOC:

$ < /dev/zero pv -s 100G -S > /dev/null
 100GiB 0:00:10 [9,98GiB/s] [=============================>] 100%            

Quoi??? Près de 10 Go / s ? Comment le retrait catet un tuyau peuvent-ils plus que tripler la vitesse? Si vous utilisez une source plus lente telle que /dev/urandomla différence est négligeable. Est- pvce que faire de la magie vaudou? J'ai donc testé:

$ dd if=/dev/zero iflag=count_bytes count=200G of=/dev/null status=progress
205392969728 bytes (205 GB, 191 GiB) copied, 16 s, 12,8 GB/s

12,8 Go / s ! Même approximation que pv, et 4 fois plus rapide que l'utilisation de tuyaux.

Est-ce catà blâmer? Les tuyaux sont-ils si différents de la redirection? Après tout, les deux vont pvcomme stdin, non? Qu'est-ce qui peut expliquer cette énorme différence?

Réponses

8 StephenKitt Sep 04 2020 at 03:48

Le tueur est l'utilisation de deux processus.

Avec cat | pv, catlit et écrit, pvlit et écrit, et les deux processus doivent s'exécuter:

$ perf stat sh -c 'cat /dev/zero | pv -s 100G -S > /dev/null'
 100GiB 0:00:26 [3.72GiB/s] [====================================================================================>] 100%            

 Performance counter stats for 'sh -c cat /dev/zero | pv -s 100G -S > /dev/null':

         34,048.63 msec task-clock                #    1.267 CPUs utilized          
         1,676,706      context-switches          #    0.049 M/sec                  
             3,678      cpu-migrations            #    0.108 K/sec                  
               304      page-faults               #    0.009 K/sec                  
   119,270,941,758      cycles                    #    3.503 GHz                      (74.89%)
   137,822,862,590      instructions              #    1.16  insn per cycle           (74.94%)
    32,379,369,104      branches                  #  950.974 M/sec                    (75.14%)
       216,658,446      branch-misses             #    0.67% of all branches          (75.04%)

      26.865741948 seconds time elapsed

       1.257950000 seconds user
      38.893870000 seconds sys

Avec pvseulement, il n'y a que la pvlecture et l'écriture, pas de changement de contexte nécessaire (ou presque pas):

$ perf stat sh -c '< /dev/zero pv -s 100G -S > /dev/null'
 100GiB 0:00:07 [13.3GiB/s] [====================================================================================>] 100%            

 Performance counter stats for 'sh -c < /dev/zero pv -s 100G -S > /dev/null':

          7,501.68 msec task-clock                #    1.000 CPUs utilized          
                37      context-switches          #    0.005 K/sec                  
                 0      cpu-migrations            #    0.000 K/sec                  
               198      page-faults               #    0.026 K/sec                  
    27,916,420,023      cycles                    #    3.721 GHz                      (75.00%)
    62,787,377,126      instructions              #    2.25  insn per cycle           (74.99%)
    15,361,951,954      branches                  # 2047.801 M/sec                    (75.03%)
        51,741,595      branch-misses             #    0.34% of all branches          (74.98%)

       7.505304560 seconds time elapsed

       1.768600000 seconds user
       5.733786000 seconds sys

Il existe un certain parallélisme («1.267 CPUs utilisés»), mais cela ne compense pas l'énorme différence dans le nombre de changements de contexte.

Les choses pourraient être pires, compte tenu du chemin des données - dans le premier cas, les données semblent couler du noyau ( /dev/zero), vers cat, vers le noyau (pour le tube), vers pv, vers le noyau ( /dev/null). Dans le second, les données circulent du noyau pvvers le noyau. Mais dans le premier scénario, pvutilise splicepour copier les données du tube, évitant un voyage dans la mémoire appartenant au noyau.