2 votes

Pire pour les performances concomitantes que séquentielles

J'ai écrit une fonction qui suréchantillonne un fichier de 48kHz à 192kHz au moyen d'un filtre :

upsample :: Coefficients -> FilePath -> IO ()

Il prend les coefficients du filtre, le chemin du fichier (qui doit être upsamples) et écrit le résultat dans un nouveau fichier.

Je dois échantillonner à la hausse de nombreux fichiers, j'ai donc écrit une fonction pour échantillonner à la hausse un répertoire complet en parallèle, en utilisant forConcurrently_ de Control.Concurrent.Async :

upsampleDirectory :: Directory -> FilePath -> IO ()
upsampleDirectory dir coefPath = do
  files <- getAllFilesFromDirectory dir
  coefs <- loadCoefficients coefPath
  forConcurrently_ files $ upsample coefs

Je compile avec le -threaded et l'exécution en utilisant +RTS -N2 . Ce que je constate, c'est que le sur-échantillonnage de 2 fichiers de manière séquentielle est plus rapide que le sur-échantillonnage des deux fichiers en parallèle.

Sur-échantillonnage file1.wav prend 18,863s. Sur-échantillonnage file2.wav prend 18,707s. Le suréchantillonnage d'un répertoire avec file1.wav y file2.wav prend 66.250s.

Qu'est-ce que je fais de mal ?

J'ai essayé de garder cet article concis, alors n'hésitez pas à me demander si vous avez besoin de plus de détails sur certaines fonctions.

2voto

K. A. Buhr Points 14622

Voici quelques possibilités. D'abord, assurez-vous à 100% que vous exécutez réellement votre programme avec +RTS -N2 -RTS . Je ne peux pas vous dire combien de fois j'ai été en train de benchmarker un programme parallèle et j'ai écrit :

stack exec myprogram +RTS -N2 -RTS

à la place de :

stack exec myprogram -- +RTS -N2 -RTS

et je me suis désespérément embrouillé. (La première version exécute le exécutable de la pile sur deux processeurs mais l'exécutable cible sur un seul !) Peut-être ajouter un print $ getNumCapabilities au début de votre main pour être sûr.

Après avoir confirmé que vous fonctionnez sur deux processeurs, le problème le plus probable est que votre implémentation ne fonctionne pas en espace constant et fait exploser le tas. Voici un programme de test simple que j'ai utilisé pour essayer de reproduire votre problème. (N'hésitez pas à utiliser vous-même mon superbe filtre d'upsampling).

module Main where

import Control.Concurrent.Async
import System.Environment
import qualified Data.ByteString as B

upsample :: FilePath -> IO ()
upsample fp = do c <- B.readFile fp
                 let c' = B.pack $ concatMap (replicate 4) $ B.unpack c
                 B.writeFile (fp ++ ".out") c'

upsampleFiles :: [FilePath] -> IO ()
upsampleFiles files = do
  forConcurrently_ files $ upsample

main :: IO ()
main = upsampleFiles =<< getArgs   -- sample all file on command line

Lorsque je l'ai fait fonctionner sur un seul fichier de test de 70 mégaoctets, il s'est exécuté en 14 secondes. Lorsque je l'ai exécuté sur deux copies en parallèle, il a fonctionné pendant plus d'une minute avant de commencer à échanger comme un fou, et j'ai dû le tuer. Après être passé à :

import qualified Data.ByteString.Lazy as B

il a fonctionné en 3,7 secondes sur un seul fichier, 7,8 secondes sur deux copies sur un seul processeur, et 4,0 secondes sur deux copies sur deux processeurs avec +RTS -N2 .

Assurez-vous que vous compilez avec les optimisations activées, profilez votre programme, et assurez-vous qu'il s'exécute dans un espace de tas constant (ou au moins raisonnable). Le programme ci-dessus s'exécute dans un espace de tas constant de 100k octets. Une version similaire qui utilise un ByteString pour la lecture et le farniente ByteString pour l'écriture lit tout le fichier en mémoire, mais le tas augmente presque immédiatement jusqu'à 70 mégaoctets (la taille du fichier) en une fraction de seconde et reste ensuite constant pendant le traitement du fichier.

Quelle que soit la complexité de votre filtre, si votre programme fait croître des gigaoctets de tas, l'implémentation est défectueuse et vous devrez la corriger avant de vous préoccuper des performances, parallèles ou autres.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X