Programmare con i pool di memoria

Almost every developer who has used the C programming language has at some point sighed at the daunting task of managing memory usage. Allocating enough memory to use, keeping track of those allocations, freeing the memory when you no longer need it—these tasks can be quite complex. And of course, failure to do those things properly can result in a program that crashes itself, or worse, crashes the computer. Fortunately, the APR library that Subversion depends on for portability provides the apr_pool_t type, which represents a pool from which the application may allocate memory.

Quasi ogni sviluppatore che ha utilizzato il linguaggio di programmazione C a qualche punto ha sospirato allo scoraggiante compito di gestire l'uso della memoria. Allocare abbastanza memoria da utilizzare, mantenere traccia di quelle allocazioni, liberare la memoria quando non è più necessaria—questi compiti possono essere abbastanza complessi. E naturalmente, fallire a fare queste cose correttamente può risultare in un programma che va in crash, o peggio, fa andare in crash il computer. Fortunatamente, la libreria APR da cui Subversion dipende per portabilità fornisce il tipo apr_pool_t, che rappresenta un pool dal quale l'applicazione può allocare memoria.

A memory pool is an abstract representation of a chunk of memory allocated for use by a program. Rather than requesting memory directly from the OS using the standard malloc() and friends, programs that link against APR can simply request that a pool of memory be created (using the apr_pool_create() function). APR will allocate a moderately sized chunk of memory from the OS, and that memory will be instantly available for use by the program. Any time the program needs some of the pool memory, it uses one of the APR pool API functions, like apr_palloc(), which returns a generic memory location from the pool. The program can keep requesting bits and pieces of memory from the pool, and APR will keep granting the requests. Pools will automatically grow in size to accommodate programs that request more memory than the original pool contained, until of course there is no more memory available on the system.

Un pool di memoria è una rappresentazione astratta di un pezzo di memoria allocata per l'uso da parte di un programma. Piuttosto che richiedere la memoria direttamente al sistema operativo utilizzando la funzione standard malloc() e simili, i porgrammi che si collegano a APR possono semplicemente richiedere che venga creato un pool di memoria (utilizzando la funzione apr_pool_create()). APR allocherà un pezzo di memoria di moderata grandezza dal sistema operativo, e questa memoria diverrà istantaneamente disponibile per l'uso da parte del programma. Ogni volta che il programma necessita di qualche pool di memoria, utilizza una delle funzioni dell'API per i pool di APR, come apr_palloc(), che ritorna una posizione di memoria generica dal pool. Il programma può continuare a chiedere bit e pezzi di memoria dal pool, e APR continuerà a esaudire le richieste. I pool cresceranno automaticamente in grandezza per accogliere i programmi che richiedono più memoria di quella che il pool originale conteneva, naturalmente fino a quando non ci sia più memoria disponibile nel sistema.

Now, if this were the end of the pool story, it would hardly have merited special attention. Fortunately, that's not the case. Pools can not only be created; they can also be cleared and destroyed, using apr_pool_clear() and apr_pool_destroy() respectively. This gives developers the flexibility to allocate several—or several thousand—things from the pool, and then clean up all of that memory with a single function call! Further, pools have hierarchy. You can make subpools of any previously created pool. When you clear a pool, all of its subpools are destroyed; if you destroy a pool, it and its subpools are destroyed.

Ora, se questa fosse stata la fine della storia dei pool, a stento avrebbe meritato speciale attenzione. Fortunatamente, questo non è il caso. I pool possono non solo essere creati; possono anche essere puliti e distrutti, utilizzando rispettivamente apr_pool_clear() e apr_pool_destroy(). Questo da agli sviluppatori la flessibilità di allocare varie—o varie migliaia—cose dal pool, e poi pulire tutta questa memoria con una singola chiamata a funzione! Inoltre, i pool hanno una gerarchia. Si possono fare dei «sotto pool» di ogni pool creato precedentemente. Quando si pulisce un pool, tutti i suoi sotto pool vengono distrutti; se si distrugge un pool, questo e i suoi sotto pool vengono distrutti.

Before we go further, developers should be aware that they probably will not find many calls to the APR pool functions we just mentioned in the Subversion source code. APR pools offer some extensibility mechanisms, like the ability to have custom user data attached to the pool, and mechanisms for registering cleanup functions that get called when the pool is destroyed. Subversion makes use of these extensions in a somewhat non-trivial way. So, Subversion supplies (and most of its code uses) the wrapper functions svn_pool_create(), svn_pool_clear(), and svn_pool_destroy().

Prima di andare oltre, gli sviluppatori dovrebbero essere informati che probabilmente non troveranno nel codice sorgente di Subversion molte delle chiamate alle funzioni di APR per i pool che abbiamo appena menzionato. I pool di APR offrono qualche meccanismo di estendibilità, come l'abilità di avere «dati utente» personalizzati attaccati al pool, e meccanismi per registrare funzioni di pulizia che possono essere chiamate quando il pool viene distrutto. Subversion fa uso di queste estensioni in un modo piuttosto non banale. Così, Subversion fornisce (e la maggior parte del proprio codice utilizza) le funzioni di avvolgimento svn_pool_create(), svn_pool_clear(), e svn_pool_destroy().

While pools are helpful for basic memory management, the pool construct really shines in looping and recursive scenarios. Since loops are often unbounded in their iterations, and recursions in their depth, memory consumption in these areas of the code can become unpredictable. Fortunately, using nested memory pools can be a great way to easily manage these potentially hairy situations. The following example demonstrates the basic use of nested pools in a situation that is fairly common—recursively crawling a directory tree, doing some task to each thing in the tree.

Mentre i pool sono utili per una gestione di base della memoria, la costruzione dei pool brilla realmente negli scenari di ciclo e ricorsione. Poiché i cicli sono spesso illimitati nelle loro iterazioni, e le ricorsioni nella loro profondità, il consumo di memoria in queste aree del codice può diventare non predicibile. Fortunatamente, utilizzando pool di memoria annidati può essere un buon modo per gestire facilmente queste situazioni potenzialmente pericolose. L'esempio seguente dimostra l'uso di base dei pool annidati in una situazione che è ragionevolmente comune—l'esplorazione ricorsiva di un albero di directory, eseguendo qualche compito per ogni cosa nell'albero.

Esempio 8.5. Effective Pool Usage

/* Recursively crawl over DIRECTORY, adding the paths of all its file
   children to the FILES array, and doing some task to each path
   encountered.  Use POOL for the all temporary allocations, and store
   the hash paths in the same pool as the hash itself is allocated in.  */
static apr_status_t 
crawl_dir (apr_array_header_t *files,
           const char *directory,
           apr_pool_t *pool)
{
  apr_pool_t *hash_pool = files->pool;  /* array pool */
  apr_pool_t *subpool = svn_pool_create (pool);  /* iteration pool */
  apr_dir_t *dir;
  apr_finfo_t finfo;
  apr_status_t apr_err;
  apr_int32_t flags = APR_FINFO_TYPE | APR_FINFO_NAME;

  apr_err = apr_dir_open (&dir, directory, pool);
  if (apr_err)
    return apr_err;

  /* Loop over the directory entries, clearing the subpool at the top of
     each iteration.  */
  for (apr_err = apr_dir_read (&finfo, flags, dir);
       apr_err == APR_SUCCESS;
       apr_err = apr_dir_read (&finfo, flags, dir))
    {
      const char *child_path;

      /* Clear the per-iteration SUBPOOL.  */
      svn_pool_clear (subpool);

      /* Skip entries for "this dir" ('.') and its parent ('..').  */
      if (finfo.filetype == APR_DIR)
        {
          if (finfo.name[0] == '.'
              && (finfo.name[1] == '\0'
                  || (finfo.name[1] == '.' && finfo.name[2] == '\0')))
            continue;
        }

      /* Build CHILD_PATH from DIRECTORY and FINFO.name.  */
      child_path = svn_path_join (directory, finfo.name, subpool);

      /* Do some task to this encountered path. */
      do_some_task (child_path, subpool);

      /* Handle subdirectories by recursing into them, passing SUBPOOL
         as the pool for temporary allocations.  */
      if (finfo.filetype == APR_DIR)
        {
          apr_err = crawl_dir (files, child_path, subpool);
          if (apr_err)
            return apr_err;
        }

      /* Handle files by adding their paths to the FILES array.  */
      else if (finfo.filetype == APR_REG)
        {
          /* Copy the file's path into the FILES array's pool.  */
          child_path = apr_pstrdup (hash_pool, child_path);

          /* Add the path to the array.  */
          (*((const char **) apr_array_push (files))) = child_path;
        }
    }

  /* Destroy SUBPOOL.  */
  svn_pool_destroy (subpool);

  /* Check that the loop exited cleanly. */
  if (apr_err)
    return apr_err;

  /* Yes, it exited cleanly, so close the dir. */
  apr_err = apr_dir_close (dir);
  if (apr_err)
    return apr_err;

  return APR_SUCCESS;
}

The previous example demonstrates effective pool usage in both looping and recursive situations. Each recursion begins by making a subpool of the pool passed to the function. This subpool is used for the looping region, and cleared with each iteration. The result is memory usage is roughly proportional to the depth of the recursion, not to total number of file and directories present as children of the top-level directory. When the first call to this recursive function finally finishes, there is actually very little data stored in the pool that was passed to it. Now imagine the extra complexity that would be present if this function had to alloc() and free() every single piece of data used!

L'esempio precedente dimostra l'effettivo utilizzo del pool in entrambe le situazioni di ciclo e ricorsione. Ogni ricorsione inizia creando un sotto pool del pool passato alla funzione. Questo sotto pool è utilizzato per la regione di ciclo, e pulito con ogni iterazione. Il risultato di utilizzo della memoria è approssimativamente proporzionale alla profondità della ricorsione, non il numero totale di file e directory presenti come figli della directory di più alto livello. Quando la prima chiamata a questa funzione ricorsiva finalmente finisce, attualmente c'è un dato molto piccolo immagazzinato nel pool che le è stato passato. Ora immaginate la complessità ulteriore che sarebbe presente se questa funzione dovesse usare alloc() e free() ogni singolo pezzo di dati utilizzati!

Pools might not be ideal for every application, but they are extremely useful in Subversion. As a Subversion developer, you'll need to grow comfortable with pools and how to wield them correctly. Memory usage bugs and bloating can be difficult to diagnose and fix regardless of the API, but the pool construct provided by APR has proven a tremendously convenient, time-saving bit of functionality.

I pool possono non essere l'ideale per ogni applicazione, ma sono estremamente utili in Subversion. Come sviluppatore di Subversion, occorre prendere confidenza con i pool e come maneggiarli correttamente. Gli errori di utilizzo della memoria e la sua crescita possono essere difficili da diagnosticare e correggere malgrado l'API, ma la costruzione del pool fornita da APR è risultata tremendamente conveniente, facendo risparmiare tempo in bit di funzionalità.