La consommation mémoire de cat

Temps de lecture estimé : 4 minutes

Combien cat consomme en mémoire vive ? Cette consommation croît-elle avec la taille du fichier ? Ces questions peuvent paraître triviales, mais posez-vous la question de la fréquence où vous utilisez cet outil… et la fréquence où vous vous interrogez sur le poids du fichier que vous faites ingérer à cat. On ne se pose tout simplement pas la question, précisément parce que l’outil ne bronche pas ; tout passe, et sur toute machine. C’est génial, mais alors pourquoi ?

Quelles sont les conso sur un gros fichier ?

Pour bien étudier comment vit cat, il est préférable de travailler avec un fichier conséquent. Créons donc un fichier de 1Gib basé sur des données aléatoires :

$ dd if=/dev/urandom of=/tmp/1gib.test bs=1024 count=1048576
1048576+0 enregistrements lus
1048576+0 enregistrements écrits
1073741824 octets (1,1 GB, 1,0 GiB) copiés, 8,81306 s, 122 MB/s

On va maintenant surveiller la consommation au cours du temps. Affichons le fichier dans la sortie standard :

cat /tmp/1gib.test

Pendant qu’il tourne, ouvrons un second terminal et traçons les évolutions du processus :

$ watch -d 'ps -o cmd,pmem,sz,pcpu,cp a | head -1; ps -o cmd,pmem,sz,pcpu,cp a | grep -a "cat /tmp/1gib.test" | grep -va "grep"'

Ce qu’on voit ici est très intéressant : si le CPU fluctue, la RAM reste étonnamment constante. Une fois ce constat fait, vous pouvez couper les deux processus avec Ctrl-C.

Pourquoi ça consomme aussi peu ?

Lors du cat /tmp/1gib.test, quelque chose a dû vous sauter aux yeux : il n’y a pas d’attente, le fichier vous est affiché immédiatement et progressivement. Nous avons peut-être quelque chose à creuser là-dedans.

Cherchons à savoir ce qu’il se passe à travers les appels systèmes :

$ strace -o /tmp/strace_cat cat /tmp/1gib.test > /dev/null 2>&1
$ grep "read\|write" /tmp/strace_cat 
read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0P@\2\0\0\0\0\0"..., 832) = 832
read(3, "\4\0\0\0\24\0\0\0\3\0\0\0GNU\0Q7r6\255\1\200\216&@L\230\372\244\35r"..., 68) = 68
read(3, "\4\0\0\0\24\0\0\0\3\0\0\0GNU\0Q7r6\255\1\200\216&@L\230\372\244\35r"..., 68) = 68
read(3, "\4\0\0\0\20\0\0\0\5\0\0\0GNU\0\2\0\0\300\4\0\0\0\3\0\0\0\0\0\0\0", 32) = 32
read(3, "\212d+\312\343\362\240 \371\224;!\32\254\217\313\361\250\374Q\212\320\214nZ\202\262T\252\333\220\242"..., 131072) = 131072
write(1, "\212d+\312\343\362\240 \371\224;!\32\254\217\313\361\250\374Q\212\320\214nZ\202\262T\252\333\220\242"..., 131072) = 131072
read(3, "\302\177\377\321\376?\220\236@\362\7\267\354\356g\271xa\367\361\321!\242\306:U\223@\261\vJ\271"..., 131072) = 131072
write(1, "\302\177\377\321\376?\220\236@\362\7\267\354\356g\271xa\367\361\321!\242\306:U\223@\261\vJ\271"..., 131072) = 131072
[...]
read(3, "e\376\270 \37\325\322\304\2347z3\264p\375\30\260\321\203Bi9c\207\25Rk\341YN$K"..., 131072) = 131072
write(1, "e\376\270 \37\325\322\304\2347z3\264p\375\30\260\321\203Bi9c\207\25Rk\341YN$K"..., 131072) = 131072
read(3, "\336\35I\36\261{\250d\232Le\204\277\266\366\4\221u\350\231\v6\231a&\323\316<\325\317K'"..., 131072) = 131072
write(1, "\336\35I\36\261{\250d\232Le\204\277\266\366\4\221u\350\231\v6\231a&\323\316<\325\317K'"..., 131072) = 131072
[...]
read(3, "\211\21\23\343\4N%t\230\242<\306\325\275L\224\343\357g\300Wu7 \26K\345)\235\3160\307"..., 131072) = 131072
write(1, "\211\21\23\343\4N%t\230\242<\306\325\275L\224\343\357g\300Wu7 \26K\345)\235\3160\307"..., 131072) = 131072
read(3, ">\305\234\337b\374'\241\214\357\316\317]\263\3l\313\305\375\27\4\355\201\227+\330f\217\36\27\272\267"..., 131072) = 131072
write(1, ">\305\234\337b\374'\241\214\357\316\317]\263\3l\313\305\375\27\4\355\201\227+\330f\217\36\27\272\267"..., 131072) = 131072
[...]
read(3, "\210\362\30\2316m\347\320\256\275\203\273\3256\207\r\204\357\237n\247\362!\213te\f\362\302\236K\350"..., 131072) = 131072
write(1, "\210\362\30\2316m\347\320\256\275\203\273\3256\207\r\204\357\237n\247\362!\213te\f\362\302\236K\350"..., 131072) = 131072
read(3, "+\335\322\210\276\245G\266\306{[m\240\256\310u\311\206\16\217l&>\375Gh\336\2723H\202;"..., 131072) = 131072
write(1, "+\335\322\210\276\245G\266\306{[m\240\256\310u\311\206\16\217l&>\375Gh\336\2723H\202;"..., 131072) = 131072
read(3, "4\330\203\25\244\331\340\31n\336\232\334\224:\222=uA\4\357OGm\352C\271O]$\271\36\313"..., 131072) = 131072
write(1, "4\330\203\25\244\331\340\31n\336\232\334\224:\222=uA\4\357OGm\352C\271O]$\271\36\313"..., 131072) = 131072
[...]
read(3, "\340r\257X]y+@\212\227*\4\312\274.\352\266(\362\363\210i\226\34\31\202\210\201\354/\334j"..., 131072) = 131072
write(1, "\340r\257X]y+@\212\227*\4\312\274.\352\266(\362\363\210i\226\34\31\202\210\201\354/\334j"..., 131072) = 131072
read(3, "\305Q=\303D\224\204\346.4\303R\356\273h'\217\207z\274\305\310\222j\f\313\277\202\26\2610\35"..., 131072) = 131072
write(1, "\305Q=\303D\224\204\346.4\303R\356\273h'\217\207z\274\305\310\222j\f\313\277\202\26\2610\35"..., 131072) = 131072
[...]

L’étude de la trace montre plusieurs choses notables :

  1. passées quelques lignes d’initialisation, nous observons une répétition de lectures et écritures du même contenu,
  2. une valeur revient sans cesse sur ces lectures / écritures : 131072.

Ce que nous pouvons en déduire, c’est que la lecture du fichier est progressive et envoyée directement à la sortie. La taille du tampon d’entrée / sortie est 131072o / 1024 = 128Kio.

Confrontons cette analyse au code de cat lui-même :

// https://github.com/coreutils/coreutils/blob/v9.1/src/cat.c#L154
static bool
simple_cat (char *buf, idx_t bufsize)
{
  /* Loop until the end of the file.  */

  while (true)
    {
      /* Read a block of input.  */

      size_t n_read = safe_read (input_desc, buf, bufsize);
      if (n_read == SAFE_READ_ERROR)
        {
          error (0, errno, "%s", quotef (infile));
          return false;
        }

      /* End of this file?  */

      if (n_read == 0)
        return true;

      /* Write this block out.  */

      if (full_write (STDOUT_FILENO, buf, n_read) != n_read)
        die (EXIT_FAILURE, errno, _("write error"));
    }
}

Cherchons également la taille standard d’un bloc (bufsize) :

// https://github.com/coreutils/coreutils/blob/v9.1/src/ioblksize.h#L24
/* As of May 2014, 128KiB is determined to be the minimium
   blksize to best minimize system call overhead.
   This can be tested with this script:
   [...]
   */
enum { IO_BUFSIZE = 128 * 1024 };

L’interprétation était la bonne. En procédant de façon séquentielle : lecture -> écriture -> recommencer, cat conserve une empreinte mémoire la plus faible possible vu qu’il évite l’accumulation sur la RAM. De cette façon, la taille du fichier initial lui importe peu, on pourrait même travailler sur un fichier virtuellement infini si on le voulait.

Ce qu’on peut en déduire

Cette façon de procéder pour s’abstraire de la taille de la donnée initiale est élégante et n’est pas conscrite à cat, ce n’est ni plus ni moins que du streaming de données.

Dans le monde du web où les échanges visent le temps réel, on s’intéresse davantage à la réactivité envers l’utilisateur qu’à la consommation de ressources (quoique!). Dans ce contexte, le streaming devient une fonctionnalité très efficace puisqu’il peut offrir les deux. Il existe plusieurs façons de faire : webtorrent, websocket ou streaming RPC, chacune avec ses avantages, inconvénients et particularités d’architecture.

En conclusion, les développeurs de cat nous offrent un mantra précieux : « lorsque la taille de la source est inconnue, paginez la réponse ! »

Comme d’habitude, vous pouvez retrouver le code pour essayer par vous-même sur le dépôt d’exemple.


Sources :


    Ce billet vous a plu ? Partagez-le sur les réseaux…


    … Ou inscrivez-vous à la newsletter pour ne manquer aucun article (Si vous ne voyez pas le formulaire, désactivez temporairement uBlock).

    Voir aussi