[Résolu] Filtrer une array: predicate ? block?

berfisberfis Membre
mai 2013 modifié dans API AppKit #1

Bonsoir,


 


Je suis à  la recherche de solutions pour filtrer une array avant un deuxième traitement, qui pourrait alors être appliqué sans test supplémentaire.


 


Mettons que je veuille supprimer d'un dossier tous les fichiers qui dépassent 500Ko. J'ai le choix entre:


 


1) obtenir un énumérateur, puis pour tous les éléments, faire un test sur la taille et détruire les fichiers trop gros;


2) obtenir un énumérateur, le filtrer en fonction de la taille, puis détruire tous les fichiers qui restent.


 


J'aimerais savoir comment implémenter la solution B. Je pense à  un autre traitement, de type réduction d'image, qui pourrait être appliqué à  plusieurs reprises. Comme à  chaque passage, les fichiers se réduisent en taille, l'énumérateur devient de plus en plus réduit, jusqu'à  ce qu'il soit vide. A ce moment, le traitement est terminé.


 


Dès lors, comment faire? J'ai vu moults exemples de prédicats @SELF contains[c] 'e', avec de petites variantes, mais jamais de SELF.size > 500000...


 


J'en déduis qu'il faudrait en passer par un block, mais là  j'avoue franchement être largué.


 


Un coup de main, svp... merci!


Réponses

  • AlakAlak Membre

    Quels sont les objets dans ce tableau? 


     


    L'idée c'est de faire :


     



    NSArray *newArray = [myArray filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@SELF.size > 500000]];
  • Pour info, il est mieux d'utiliser la méthode -predicateWithBlock: plutôt que withFormat:, surtout pour filtrer de grosses array ou pour des opérations répétées. J'ai remarqué pendant des tests de perfs que -predicateWithBlock: est beaucoup plus performant (en plus d'être beaucoup plus "libre")


  • Ce que je tend à  reprocher au quelqueChoseWithBlock: c'est que ce qui se fait dans le bloc ne se fait pas dans le même process( de ça je ne suis pas certain ) : il faut faire le traitement à  l'intérieur du bloc <=> on ne peut pas travailler directement à  sa sortie. Un simple NSOpenPanel devient compliqué à  utiliser si on veut travailler sur [openPanel URL] après le bloc ..


    Sûrement quelque chose qui m'échappe mais je n'ai pas pris le temps de chercher quoi : je fais mes traitements entre les {} du block, bête et discipliné  >:)


  • AliGatorAliGator Membre, Modérateur

    Heu j'ai pas tout compris de ta remarque laudema...


     


    Pour le quelqueChoseWithBlock de toute façon ça dépend fortement si le qqch est synchrone ou asynchrone. Les blocs, faut voir ça un peu comme le pattern "Command". Certes c'est utilisé parfois pour de l'asynchrone, donc certains ont la mauvaise impression que blocks = GCD = asynchrone, alors qu'en fait c'est pas forcément lié (oui GCD utilise les blocks mais les blocks s'utilisent pas qu'avec GCD)


     


    Par exemple la méthode filteredArray... qui utilise un block, ça prend un block en paramètre certes, c'est à  dire "un bout de code", qu'il va exécuter pour chaque élément du tableau (en passant cet élément en paramètre au block) pour savoir s'il garde cet élément dans le tableau filtré ou pas. Mais cette méthode est synchrone (elle va filtrer le tableau, te retourner un tableau filtré en résultat, et seulement ensuite te retourner la main pour exécuter le reste du code qui suit).


     


    Une méthode de NSOpenPanel qui prend un block, elle va plutôt utiliser ce block en tant que "completion block" autrement dit toujours "un bout de code" mais qu'il va exécuter non pas immédiatement, mais "quand il aura fini", "quand l'utilisateur aura enfin choisi le fichier à  ouvrir. Du coup là  c'est de l'asynchrone. Mais pas parce que ça utilise un block, parce que c'est la méthode de NSOpenPanel qui est asycnrhone. L'autre méthode de NSOpenPanel qui fait le mm genre de chose mais sans block, elle utilise un delegate... et c'est le même fonctionnement, la méthode de delegate est appellée quand l'utilisateur a choisi son fichier et a validé. Bah là  c'est pareil, sauf que le code au lieu de le mettre dans la méthode de delegate tu le mets dans un block que tu passes en paramètre.


    Et donc oui là  forcément, le code après l'appel de NSOpenPanel va continuer de s'exécuter sans attendre (fonctionnement asynchrone), alors que le code que tu as mis dans le block lui s'exécutera plus tard, quand l'utilisateur aura validé le dialogue... mais ça c'est pareil si tu utilisais un delegate et mettais le code dans la méthode déléguée plutôt que le passer dans un block.


     


     


    Bref, c'est pas l'utilisation des blocks qui change quoi que ce soit dans l'histoire. Y'a des méthodes synchrones (comme les méthodes de NSArray pour filtrer un tableau) et des méthodes asynchrones (comme les méthodes de NSOpenPanel qui rendent la main et te préviennent plus tard quand l'utilisateur a sélectionné son/ses fichier(s)), mais ça n'a rien à  voir avec le fait que ces méthodes prennent des blocks ou non.


  • berfisberfis Membre
    avril 2013 modifié #6

    Merci à  tous pour ces réponses rapides et détaillées (particulièrement à  "La Doc c'est moi" AliGator, qui utilise un langage aisément compréhensible -- pour moi).


     


    La syntaxe des blocks me rebute un peu, mais je comprends mieux l'avantage de ce système, particulièrement pour le (détestablement) asynchrone NSOpenPanel, qui découpe mon code en rondelles (1. j'affiche, 2. je traite 3. j'exécute sur la base d'un contexte transmis en paramètre). J'avais trouvé sur un autre site (http://macresearch.org/cocoa-scientists-xxxii-10-uses-blocks-cobjective-c) cette démonstration qui "ramenait" le code pertinent à  l'intérieur d'une seule méthode (et donc en contexte). En fait, il s'agit d'une facilité rendant le code plus lisible, puisque le block est exécuté au même moment que le "completion selector" l'aurait été.


     


    Ces explications me permettent de voir l'avantage que présentent les blocks (reste à  m'y lancer). La remarque de ldesroziers m'y encourage: je suis arrivé à  l'Objective-C par la petite porte d'ApplescriptObjC, le gain de vitesse était époustouflant au début, mais depuis je m'y suis habitué et je commence à  penser café quand la durée d'exécution d'une boucle dépasse la demi-seconde...  ^_^


    Je vais donc tester cette solution "predicateWithBlock", probablement suer un peu, et je reviendrai rendre compte.


     


    Merci encore!


  • Bref, c'est pas l'utilisation des blocks qui change quoi que ce soit dans l'histoire. Y'a des méthodes synchrones (comme les méthodes de NSArray pour filtrer un tableau) et des méthodes asynchrones (comme les méthodes de NSOpenPanel qui rendent la main et te préviennent plus tard quand l'utilisateur a sélectionné son/ses fichier(s)), mais ça n'a rien à  voir avec le fait que ces méthodes prennent des blocks ou non.


    Merci Ali, vu comme ça j'hésiterais moins à  m'en servir )


    De mémoire j'avais été échaudé avec un code du genre



    __block NSURL *anURL = nil;
    NSOpenPanel *op = [NSOpenPanel openPanel];
    [op beginSheetModalForWindow:self.window completionHandler:^(NSInteger result) {
    if (NSFileHandlingPanelCancelButton != result) {
    anURL = [[op URL] retain];
    }
    }];
    if ( nil == anURL) {
    printf("failed\n");
    return;
    }

    /*console : failed*/

     


    Et comme chat échaudé craint l'eau froide (et Alf ;) ) j'ai préféré me tenir à  l'écart. Mais je m'en servirais plus facilement à  l'avenir, merci :)


     


    A propos il y a un bug dans cette méthode, mais comme c'est seulement en debugg ça ne gêne pas trop : si tu actives le breakpoint sur All Exceptions tu n'arrives pas au bout de la méthode beginSheetModalForWindow:completionHandler:, un arrêt sur le thread consacré à  QuickLook te force à  tuer l'application en entier..


  • AliGatorAliGator Membre, Modérateur
    avril 2013 modifié #8

    En fait il faut vraiment voir les blocks comme la possibilité de "passer du code en paramètre" ou "manipuler un bloc de code comme si c'était un objet Objective-C (que tu peux stocker dans une variable, passer en paramètre, utiliser plus tard...)"


     


     


     


    Un exemple tout bête, imagine que tu as une classe à  toi qui affiche par exemple une vue avec un bouton et plus tard, quand l'utilisateur cliques sur le bouton, ça va exécuter le reste du code. On retrouve ce genre de situation dans pas mal de cas, par exemple un NSOpenPanel mais pas que, c'est une situation plutôt courante.


     


    1) Le cas classique que tu connais bien, c'est quand ce genre de truc est implémenté avec des delegate. Ca donnerait un truc comme ça à  l'usage dans ton code :



    -(void)afficheTruc {
      self.truc = [[Truc alloc] init];
      self.truc.delegate = self;
      [self.truc show];
    }
     
    // Puis plus bas, implémentation de la méthode delegate
    - (void)trucDidMakeAction:(Truc*)truc {
      NSLog(@L'utilisateur a enfin décidé de cliquer sur le bouton de %@ !", truc);
      // ... ton code
    }

    2) Bah si tu "migrais" ce code avec une API qui utilise des blocs, ça ne serait pas beaucoup plus compliqué : au lieu de fixer un delegate puis de mettre le code de trucDidMakeAction plus bas, tu mettrais le code de trucDidMakeAction directement dans afficheTruc. En fonction de l'API ça pourrait donner un truc comme ça :


     


     



    -(void)afficheTruc {
      self.truc = [[Truc alloc] init];
      self.truc.completion = ^{
        NSLog(@L'utilisateur a enfin décidé de cliquer sur le bouton de %@ !", truc);
        // ... ton code
      };
      [self.truc show];
    }

    Tu remarqueras que je n'ai fait qu'enlever le self.truc.delegate = self, et couper/coller le code de trucDidMakeAction pour l'affecter à  la place à  l'hypothétique propriété "completion". Oui j'ai tout simplement affecté un bloc de code à  une variable comme j'affecterai une NSString ou un entier. C'est ça toute la puissance des blocs.


     


    Et donc ? Bah c'est tout ! Tu vois bien qu'il n'y a fondamentalement pas de différence pour ce genre de cas d'exemple entre mettre ton code dans le delegate ou le passer en paramètre via un block.


    Sauf que les blocks sont bien plus puissants, puisqu'ils permettent d'éviter d'éparpiller le code à  divers endroits de ton fichier, et permettent à  la place de grouper tout le code (même si le code est asynchrone dans le sens où le code que tu as mis dans la methode delegate/completion sera appelé "plus tard", au niveau du code tu peux tout de suite lire ça comme "affiche Truc et quand t'aura fini fais mon code"), et en plus tu remarqueras que dans le block je peux directement utiliser "self.truc", sans avoir à  le passer en paramètre ou me traà®ner une sortie de "context" pour le retrouver !


     


    Bref, je conçois tout à  fait que la syntaxe des blocks au début ça déroute, c'est sûr que c'est pas forcément évident à  la première lecture... mais faut vraiment pas en avoir peur, c'est vraiment que "des blocs de code que tu peux manipuler comme des objets et mettre dans des variables ou passer en paramètre", et pour le reste ça change pas le principe, tu mets le même code dans ton delegate ou si tu le passes par block ;)



  • // NSPredicate *predicate = [NSPredicate predicateWithBlock: ^BOOL (id evaluatedObject, NSDictionary *bindings){
    NSNumber *fileSize;
    [evaluatedObject getResourceValue:&fileSize forKey:NSURLFileSizeKey error:NULL];
    return [fileSize unsignedLongLongValue] > self.minFileSize*1000;
    }];
    //
    for (NSString *element in arr) {
    NSArray *files = [[NSFileManager defaultManager] contentsOfDirectoryAtURL:[NSURL fileURLWithPath:element]
    includingPropertiesForKeys:@[NSURLFileSizeKey]
    options:NSDirectoryEnumerationSkipsHiddenFiles error:nil];
    do {
    files = [files filteredArrayUsingPredicate:predicate];
    ...


     


    Génial, ça marche! Je n'ai plus qu'à  faire un do {...} while (files.count >0) pour voir mon traitement d'images devenir de plus en plus rapide! Merci pour ces précieuses secondes gagnées!


  • AliGatorAliGator Membre, Modérateur

    Question ouverte : je me demande au niveau de ton algo si ça ne serai pas plus efficace de faire ta réduction de taille image par image.


     


    Je veux dire plutôt que de boucler sur tous les fichiers une première fois, et faire une seule passe de traitement sur chaque images qui respecte le predicate (size > minFileSize), puis reboucler à  nouveau sur les fichiers restants une 2e fois pour faire une 2e passe sur les images qui sont encore >minFileSize, et reboucler encore sur tous les fichiers... jusqu'à  ne plus en trouver aucun


     


    Moi déjà  j'aurai plutôt fait, en solution 1 : boucler sur tout les fichiers, filtrer pour ne garder que ceux >minFileSize comme tu as fait... et sur ces images >minFileSize filtrées, j'applique le traitement d'image autant de fois que nécessaire jusqu'à  ce que le résultat soit ≤minFileSize.


    C'est une première optimisation qui éviterai de boucler plusieurs fois sur ta liste de fichiers jusqu'à  ce qu'elle ne contient plus rien, mais à  la place va traiter chaque fichier jusqu'à  ce qu'il respecte le critère "<minFileSize" avant de passer au suivant.


     


    Du coup si tu pars là  dessus, plutôt que de partir sur ta liste de fichiers, puis la filtrer avec un block (ce qui va parcourir ton tableau une première fois pour faire l'élimination), puis faire une boucle for sur le tableau filtré... autant exécuter directement le code de traitement dès la première énumération !


     


     


     


     


    Après, je ne sais pas si cette solution 1 change des masses en terme de performances, ça doit se valoir (faudrait faire les calculs de complexité d'algorithme, mais j'ai la flemme ^^). Mais du coup ça m'amène à  la solution 2 : plutôt que d'ouvrir chaque fichier, compresser ses données (je suppose que pour ça tu réenregistres la même image en JPG mais avec une qualité ou une taille d'image moindre ?), les réenregistrer sur disque, puis recommencer, autant n'ouvrir le fichier qu'une seule fois, compresser les NSData (la représentation JPG de ton image) autant que nécessaire, et n'enregistrer qu'une seule fois à  la fin. Ca fait beaucoup moins d'accès disque et de lectures/écritures.


     


    Et en plus, en traitant chaque fichier chacun de son côté, tu peux facilement paralléliser le tout pour qu'ils soient traités en parallèle et non en série !


     


    for (NSString* element in arr) {
      NSArray* files = [[NSFileManager defaultManager] contentsOfDirectory...];
     
      [files enumerateObjectsWithOptions: NSEnumerationConcurrent /* parallélisation de l'itération sur les éléments */
                         usingBlock:^(id filePath, NSUInteger idx, BOOL *stop) {
        NSNumber *fileSize;
        [filePath getResourceValue:&fileSize forKey:NSURLFileSizeKey error:NULL];
        if ([fileSize unsignedLongLongValue] > self.minFileSize*1000) {
           // je te conseille même d'appeler la méthode ci-dessous sur une queue GCD dédiée par exemple, pour que le traitement des fichiers soit fait en arrière plan et éviter de bloquer ton thread courant !
           [self processImageFile:filePath];
        }
    }
     
    - (void)processImageFile:(NSString*)filePath {
      // On ne lit les données du fichier qu'une seule fois, au début
      NSData* data = [NSData dataWithContentsOfFile:filePath];
      // On boucle tant que la taille de cette représentation binaire est trop grande
      while(data.length > self.minFileSize*1000) {
        // Traitement d'image pour faire une passe de réduction sur la taille de l'image
        data = [self applyOnePassOfImageProcessing:data];
      }
     // On ne réécrit le fichier qu'une seule fois, à  la fin, quand les données ont été assez réduites
     [data writeToFile:filePath atomically:YES];
    }

  • berfisberfis Membre
    avril 2013 modifié #11

    Oui, c'est une autre solution, en effet. Question de style, quasiment. ça doit se jouer sur des microsecondes, non?


     


    Le problème avec le multithreading, c'est que je veux au moins faire un "bip" quand j'ai fini le traitement (ce qui se produit en général une fraction de seconde après avoir traité 1000, 2000 fichiers -- j'ai donc carrément renoncé au NSProgressIndicator ^_^ ). Si j'exécute le fil principal, ça va biper avant d'avoir terminé, non?


     


    Puisque nous en sommes aux questions ouvertes: au niveau performance, au temps où j'utilisais le Pascal, j'avais lu que la solution "en ligne" était plus efficace que l'appel à  une routine, qui nécessitait le chargement du code en mémoire à  chaque fois. Est-ce toujours valable 20 ans après dans un environnement différent?


  • AliGatorAliGator Membre, Modérateur

    Oui, c'est une autre solution, en effet. Question de style, quasiment. ça doit se jouer sur des microsecondes, non?

    Bah ça ça dépend de ton nombre de fichiers à  traiter, de la performance de ta méthode de traitement d'image, mais aussi de la vitesse de ton disque dur. Le gros avantage que je vois de ma solution n'est pas tant d'inverser les boucles (boucler sur les fichiers et pour chaque fichier exécuter autant de passes de traitement que nécessaire, plutôt qu'exécuter une seule passe sur chaque fichier puis reboucher sur les fichiers) car en effet ça ça ne doit pas changer grand chose je pense. Non le gros avantage est la réduction du nombre d'accès disque. Ma solution 2 t'évite d'ouvrir 15 fois le fichier et le réécrire 15 fois si jamais il avait besoin de 15 passes de traitement d'image pour tomber sous minFileSize. Sur un SSD ca ne se vera sans doute pas trop (mais il appréciera la réduction du nombre d'écritures), sur un disque dur 5400 tours très fragmenté ça commencera à  être appréciable.


     

    Le problème avec le multithreading, c'est que je veux au moins faire un "bip" quand j'ai fini le traitement (ce qui se produit en général une fraction de seconde après avoir traité 1000, 2000 fichiers -- j'ai donc carrément renoncé au NSProgressIndicator ^_^ ). Si j'exécute le fil principal, ça va biper avant d'avoir terminé, non?

    Bah pas forcément, il suffit de placer le bip au bon endroit dans ton code. Si tu lances ton traitement asynchrone dans une queue avec disqpatch_async et que tu fais le bip APRàˆS le dispatch_async(..., ^{ ... }) alors oui forcément le bip va se faire immédiatement. Mais si tu fais le bip a la fin du bloc que tu as envoyé sur ta queue GCD, pas de soucis évidemment ! Quitte à  demander de faire le bip sur la mai queue :
    dispatch_async(processingQueue, ^{
    // ton long traitement d'image, qui va s'exécuter sur la queue GCD parallèle
    ...
    // et une fois ce long code fini, faire un bip, qu'on va exécuter sur la main queue
    dispatch_async(dispatch_get_main_queue() , ^{
    [self beep];
    });
    });

     

    Puisque nous en sommes aux questions ouvertes: au niveau performance, au temps où j'utilisais le Pascal, j'avais lu que la solution "en ligne" était plus efficace que l'appel à  une routine, qui nécessitait le chargement du code en mémoire à  chaque fois. Est-ce toujours valable 20 ans après dans un environnement différent?

    Oui et non : utiliser du code inline plutôt qu'un appel de procédure est forcément un tout petit peu plus rapide Car tu n'as pas tout l'empilement des paramètres sur la pile, etc. Mais :

    - Ce gain est plutôt négligeable vu les processeurs actuels et n'aurait d'intérêt que si tu veux faire une optimisation très poussée

    - Mais surtout, surtout, ce genre d'optimisation, les compilateurs modernes savent bien mieux les faire que toi, et savent déjà  déplier les boucles là  où c'est possible et serait plus efficace, transformer un appel de méthode en une fonction inline à  la compilation si ça permet d'optimiser... et en général du coup avec les compilateur de ces dernières années tu as tout intérêt à  laisser le compilateur déterminer lui-même si une fonction doit être inline ou pas que d'essayer de déterminer cela toi-même et risquer de mal optimiser.
  • Quand tu fais [self beep] qui est self ?


    La queue  ou l'objet qui contient dispatch_async(queue, block) ?


    Je pencherais plutôt pour l'objet si le bloc emmène avec lui son contexte d'exécution, si j'ai bien compris la doc, l'objet ne sera pas deallocated tant que block existe ?


  • AliGatorAliGator Membre, Modérateur
    C'est tout a fait ça laudema :)


    self représente le même objet que tu l'appelles à  l'intérieur ou à  l'extérieur du dispatch_async (faut voir self comme une variable tout comme si tu avais une variable x à  l'extérieur du dispatch_async et que tu l'utilisais à  l'intérieur ensuite)
  • Merci tout le monde! Mon problème de départ a été mieux que résolu, j'ai appris plein de choses grâce à  vous!


  • Suite et fin.


     


    La solution d'AliGator m'a plu, car j'essaie autant que faire se peut de limiter les accès disque. Restait à  savoir si cela faisait une différence. Il suffisait de tester massivement.


     


    J'ai donc préparé par duplications successives un ensemble de 6500 fichiers, dont la réduction de taille nécessitait entre deux et trois passages pour les ramener à  la taille voulue.


     


    Mon disque est un Western Digital 2 TB, capacité de transfert 6Go/s pour ceux qui aiment la technique.


     


    Ma solution :          28 minutes


    Solution AliGator : 19 minutes


     


    En plus, comme je ne fais plus qu'un accès par fichier, je peux afficher un NSProgressIndicator sur le nombre total, lequel ne varie plus. Ce petit chéri m'indique que les choses se passent bien et que j'ai le temps d'aller prendre mon huitième café, ce que ma méthode ne permettait pas (je ne savais même pas à  quelle "passe" elle en était). Et le Finder est bien plus réactif, aussi.


     


    Re-merci! ça c'est ce que j'appelle de l'optimisation!


Connectez-vous ou Inscrivez-vous pour répondre.