web-dev-qa-db-fra.com

Le style de codage idiomatique Scala est-il un piège génial pour écrire du code inefficace?

Je sens que la communauté Scala est un peu obsédée par l’écriture de codes "concis", "cool", "scala idiomatique" , "one-liner" - si possible -. Ceci est immédiatement suivi d’une comparaison avec le code Java/impératif/moche.

Bien que cela conduise (parfois) à une compréhension facile du code, cela conduit également à un code inefficace pour 99% des développeurs. Et c’est là que Java/C++ n’est pas facile à battre.

Considérez ce problème simple: Étant donné une liste d'entiers, supprimez le plus grand élément. La commande n'a pas besoin d'être conservée.

Voici ma version de la solution (ce n'est peut-être pas la meilleure solution, mais c'est ce que ferait un développeur moyen non rockstar).

def removeMaxCool(xs: List[Int]) = {
  val maxIndex = xs.indexOf(xs.max);
  xs.take(maxIndex) ::: xs.drop(maxIndex+1)
}

C'est Scala idiomatique, concis et utilise quelques fonctions de la liste de Nice. C'est aussi très inefficace. Il parcourt la liste au moins 3 ou 4 fois.

Voici ma solution totalement non-cool, semblable à Java. C'est également ce qu'un développeur Java raisonnable (ou novice Scala) écrirait.

def removeMaxFast(xs: List[Int]) = {
    var res = ArrayBuffer[Int]()
    var max = xs.head
    var first = true;   
    for (x <- xs) {
        if (first) {
            first = false;
        } else {
            if (x > max) {
                res.append(max)
                max = x
            } else {
                res.append(x)
            }
        }
    }
    res.toList
}

Totalement idiomatique, non fonctionnel, non concis, mais très efficace. Il ne parcourt la liste qu'une seule fois!

Ainsi, si 99% des développeurs Java écrivent un code plus efficace que 99% des développeurs Scala, il s’agit là d’un énorme obstacle à franchir pour une plus grande adoption de Scala. Y a-t-il un moyen de sortir de ce piège?

Je recherche des conseils pratiques pour éviter de tels "pièges d'inefficacité" tout en veillant à une mise en œuvre claire et concise.

Clarification: / Cette question provient d'un scénario réel: je devais écrire un algorithme complexe. Je l'ai d'abord écrit en Scala, puis je devais le réécrire en Java. L'implémentation Java était deux fois plus longue et pas si claire, mais en même temps c'était deux fois plus rapide. Réécrire le code Scala pour qu'il soit efficace prendrait probablement un peu de temps et une compréhension un peu plus approfondie des efficacités internes de Scala (pour la carte par rapport au pli, etc.)

54
Adrian

Discutons d'une erreur dans la question:

Ainsi, si 99% des développeurs Java écrivent un code plus efficace que 99% des développeurs de Scala, il s’agit d’un énorme obstacle à franchir pour une plus grande adoption de Scala . Y a-t-il un moyen de sortir de ce piège?

Ceci est présumé, sans aucune preuve à l'appui. Si faux, la question est sans objet.

Y a-t-il des preuves du contraire? Bien, considérons la question elle-même - cela ne prouve rien, mais montre que les choses ne sont pas aussi claires.

Totalement idiomatique, non fonctionnelle, non concise, mais Très efficace. Il ne parcourt la liste qu'une seule fois!

Parmi les quatre affirmations de la première phrase, les trois premières sont vraies et la quatrième, comme le montre/ utilisateur inconnu , est fausse! Et pourquoi c'est faux? Parce que, contrairement à ce que dit la deuxième phrase, elle parcourt la liste plus d’une fois.

Le code appelle les méthodes suivantes:

res.append(max)
res.append(x)

et

res.toList

Considérons d'abord append.

  1. append prend un paramètre vararg. Cela signifie que max et x sont d'abord encapsulés dans une séquence d'un type quelconque (un WrappedArray, en fait), puis transmis en tant que paramètre. Une meilleure méthode aurait été +=.

  2. Ok, append appelle ++=, qui délègue à +=. Mais, d’abord, il appelle ensureSize, qui est la deuxième erreur (+= appelle cela aussi - ++= optimise simplement cela pour plusieurs éléments). Parce qu'une Array est une collection de taille fixe, ce qui signifie qu'à chaque redimensionnement, l'intégralité de la Array doit être copiée!

Alors considérons ceci. Lorsque vous redimensionnez, Java commence par effacer la mémoire en enregistrant 0 dans chaque élément, puis Scala copie chaque élément du tableau précédent dans le nouveau tableau. Comme la taille double à chaque fois, cela se produit plusieurs fois (n), le nombre d'éléments copiés augmentant à chaque fois.

Prenons l'exemple n = 16. Il le fait quatre fois, en copiant respectivement 1, 2, 4 et 8 éléments. Puisque Java doit effacer chacun de ces tableaux et que chaque élément doit être lu et écrit, chaque élément copié représente 4 traversées d'un élément. En ajoutant tout ce que nous avons (n ​​- 1) * 4, soit environ 4 traversées de la liste complète. Si vous comptez lire et écrire comme une passe unique, comme le font souvent les gens à tort, il reste trois traversées.

On peut améliorer ceci en initialisant la ArrayBuffer avec une taille initiale égale à la liste qui sera lue, moins un, puisque nous écarterons un élément. Pour obtenir cette taille, nous devons toutefois parcourir la liste une fois.

Considérons maintenant toList. Pour le dire simplement, il parcourt toute la liste pour créer une nouvelle liste.

Nous avons donc 1 parcours pour l’algorithme, 3 ou 4 parcours pour le redimensionnement et 1 parcours supplémentaire pour toList. C'est 4 ou 5 traversées.

L'algorithme d'origine est un peu difficile à analyser car take, drop et ::: traversent un nombre variable d'éléments. En additionnant tous ensemble, cependant, cela équivaut à 3 traversées. Si splitAt était utilisé, il serait réduit à 2 traversées. Avec 2 traversées supplémentaires pour obtenir le maximum, nous obtenons 5 traversées - le même nombre que l'algorithme non fonctionnel et non concis!

Alors, considérons des améliorations.

Sur l'algorithme impératif, si l'on utilise ListBuffer et +=, alors toutes les méthodes sont à temps constant, ce qui le réduit à un seul parcours.

Sur l'algorithme fonctionnel, il pourrait être réécrit comme suit:

val max = xs.max
val (before, _ :: after) = xs span (max !=)
before ::: after

Cela le réduit à un pire cas de trois traversées. Bien sûr, il existe d’autres alternatives présentées, basées sur la récursivité ou le repli, qui la résolvent en un seul parcours.

Et, le plus intéressant de tous, tous ces algorithmes sont O(n), et le seul qui ait presque abouti (accidentellement) dans la pire complexité était l'impératif (en raison de la copie de matrice). D'autre part, les caractéristiques de cache de l'impératif pourraient bien l'accélérer, car les données sont contiguës en mémoire. Cela, cependant, n’est pas lié à big-Oh ni à fonctionnel vs impératif, il ne s’agit que des structures de données qui ont été choisies.

Donc, si nous essayons réellement de comparer, d’analyser les résultats, d’évaluer les performances des méthodes et de rechercher les moyens de l’optimiser, nous pourrons trouver des moyens plus rapides de le faire de manière impérative que fonctionnelle.

Mais tout cet effort est très différent de dire que le code de programmeur Java moyen sera plus rapide que le code de programmeur Scala moyen - si la question est un exemple, c'est simplement faux. Et même en négligeant la question, nous n’avons trouvé aucune preuve de la véracité de son postulat fondamental.

EDIT

Tout d’abord, permettez-moi de reformuler mon propos, car il me semble que je n’étais pas clair. Mon point est que le code écrit en moyenne par le programmeur Java peut sembler plus efficace, mais ne l’est pas réellement. Autrement dit, le style Java traditionnel ne vous procure pas de performances. Seul le travail ardu, que ce soit en Java ou en Scala. 

Ensuite, j’ai aussi un point de repère et des résultats, y compris presque toutes les solutions suggérées. Deux points intéressants à ce sujet:

  1. Selon la taille de la liste, la création d'objets peut avoir un impact plus important que les traversées multiples de la liste. Le code fonctionnel original d'Adrian tire parti du fait que les listes sont des structures de données persistantes en ne copiant pas les éléments à droite de l'élément maximum. Si vous utilisiez plutôt une Vector, les côtés gauche et droit resteraient pratiquement inchangés, ce qui pourrait améliorer encore les performances.

  2. Même si les utilisateurs inconnus et paradigmatiques ont des solutions récursives similaires, les paradigmatiques sont bien plus rapides. La raison en est qu'il évite les correspondances. La correspondance des motifs peut être très lente.

Le code de référence est ici et les résultats sont ici .

91
Daniel C. Sobral
def removeOneMax (xs: List [Int]) : List [Int] = xs match {                                  
    case x :: Nil => Nil 
    case a :: b :: xs => if (a < b) a :: removeOneMax (b :: xs) else b :: removeOneMax (a :: xs) 
    case Nil => Nil 
}

Voici une méthode récursive, qui n’itère qu’une fois. Si vous avez besoin de performance, vous devez y penser, sinon. 

Vous pouvez le rendre queue-récursif de la manière standard: donner un paramètre supplémentaire carry, qui est par défaut la liste vide, et collecte le résultat lors de l'itération. C'est bien sûr un peu plus long, mais si vous avez besoin de performance, vous devez payer pour cela:

import annotation.tailrec 
@tailrec
def removeOneMax (xs: List [Int], carry: List [Int] = List.empty) : List [Int] = xs match {                                  
  case a :: b :: xs => if (a < b) removeOneMax (b :: xs, a :: carry) else removeOneMax (a :: xs, b :: carry) 
  case x :: Nil => carry 
  case Nil => Nil 
}

J'ignore quelles sont les chances que les compilateurs ultérieurs améliorent les appels de carte lents pour qu'ils soient aussi rapides que les boucles while. Cependant: vous avez rarement besoin de solutions à haute vitesse, mais si vous en avez souvent besoin, vous les apprendrez rapidement. 

Savez-vous quelle doit être la taille de votre collection pour utiliser une seconde entière pour votre solution sur votre machine?

En tant qu'élément similaire à la solution Daniel C. Sobrals: 

((Nil : List[Int], xs(0)) /: xs.tail) ((p, x)=> if (p._2 > x) (x :: p._1, p._2) else ((p._2 :: p._1), x))._1

mais c’est difficile à lire et je n’ai pas mesuré la performance effective. Le modèle normal est (x /: xs) ((a, b) =>/* quelque chose * /). Ici, x et a sont des paires de List-so-far et de max-so-far, ce qui résout le problème de tout mettre dans une ligne de code, mais n'est pas très lisible. Cependant, vous pouvez gagner une réputation sur CodeGolf de cette façon, et peut-être que quelqu'un aime faire une mesure de performance. 

Et maintenant, à notre grande surprise, quelques mesures:

Une méthode de chronométrage mise à jour, pour obtenir la récupération de place, et faire chauffer le compilateur Hotspot, une méthode principale et de nombreuses méthodes de ce fil, ensemble dans un objet nommé 

object PerfRemMax {

  def timed (name: String, xs: List [Int]) (f: List [Int] => List [Int]) = {
    val a = System.currentTimeMillis 
    val res = f (xs)
    val z = System.currentTimeMillis 
    val delta = z-a
    println (name + ": "  + (delta / 1000.0))
    res
  }

def main (args: Array [String]) : Unit = {
  val n = args(0).toInt
  val funs : List [(String, List[Int] => List[Int])] = List (
    "indexOf/take-drop" -> adrian1 _, 
    "arraybuf"      -> adrian2 _, /* out of memory */
    "paradigmatic1"     -> pm1 _, /**/
    "paradigmatic2"     -> pm2 _, 
    // "match" -> uu1 _, /*oom*/
    "tailrec match"     -> uu2 _, 
    "foldLeft"      -> uu3 _,
    "buf-=buf.max"  -> soc1 _, 
    "for/yield"     -> soc2 _,
    "splitAt"       -> daniel1,
    "ListBuffer"    -> daniel2
    )

  val r = util.Random 
  val xs = (for (x <- 1 to n) yield r.nextInt (n)).toList 

// With 1 Mio. as param, it starts with 100 000, 200k, 300k, ... 1Mio. cases. 
// a) warmup
// b) look, where the process gets linear to size  
  funs.foreach (f => {
    (1 to 10) foreach (i => {
        timed (f._1, xs.take (n/10 * i)) (f._2)
        compat.Platform.collectGarbage
    });
    println ()
  })
}

J'ai renommé toutes les méthodes et ai dû modifier un peu uu2 pour l'adapter à la déclaration de méthode commune (List [Int] => List [Int]). 

À partir du résultat long, je ne fournis la sortie que pour les invocations 1M: 

scala -Dserver PerfRemMax 2000000
indexOf/take-drop:  0.882
arraybuf:   1.681
paradigmatic1:  0.55
paradigmatic2:  1.13
tailrec match: 0.812
foldLeft:   1.054
buf-=buf.max:   1.185
for/yield:  0.725
splitAt:    1.127
ListBuffer: 0.61

Les nombres ne sont pas complètement stables, en fonction de la taille de l'échantillon, et varient légèrement d'une exécution à l'autre. Par exemple, pour des exécutions de 100 000 à 1 M, par pas de 100 000, le timing de splitAt était le suivant:

splitAt: 0.109
splitAt: 0.118
splitAt: 0.129
splitAt: 0.139
splitAt: 0.157
splitAt: 0.166
splitAt: 0.749
splitAt: 0.752
splitAt: 1.444
splitAt: 1.127

La solution initiale est déjà assez rapide. splitAt est une modification de Daniel, souvent plus rapide, mais pas toujours.

La mesure a été effectuée sur un seul cœur 2Ghz Centrino, sous xUbuntu Linux, Scala-2.8 avec Sun-Java-1.6 (ordinateur de bureau). 

Les deux leçons pour moi sont: 

  • mesurez toujours vos améliorations de performances; il est très difficile de l’estimer, si vous ne le faites pas quotidiennement 
  • écrire du code fonctionnel n’est pas seulement amusant; parfois, le résultat est encore plus rapide

Voici un lien vers mon code de référence, si quelqu'un est intéressé.

25
user unknown

Tout d’abord, le comportement des méthodes que vous avez présentées n’est pas le même. Le premier conserve l'ordre des éléments, le second pas.

Deuxièmement, parmi toutes les solutions possibles que l’on pourrait qualifier d ’" idiomatiques ", certaines sont plus efficaces que d’autres. En restant très proche de votre exemple, vous pouvez par exemple utiliser la récursion pour éliminer les variables et la gestion manuelle des états:

def removeMax1( xs: List[Int] ) = {
  def rec( max: Int, rest: List[Int], result: List[Int]): List[Int] = {
    if( rest.isEmpty ) result
    else if( rest.head > max ) rec( rest.head, rest.tail, max :: result)
    else rec( max, rest.tail, rest.head :: result )
  }
  rec( xs.head, xs.tail, List() )
}

ou pliez la liste:

def removeMax2( xs: List[Int] ) = {
  val result = xs.tail.foldLeft( xs.head -> List[Int]() ) { 
    (acc,x) =>
      val (max,res) = acc
      if( x > max ) x -> ( max :: res )
      else max -> ( x :: res )
  }
  result._2
}

Si vous souhaitez conserver l'ordre d'insertion d'origine, vous pouvez (au prix de deux passes plutôt que d'une seule) écrire sans effort quelque chose comme:

def removeMax3( xs: List[Int] ) = {
  val max = xs.max
  xs.filterNot( _ == max )
}

ce qui est plus clair que votre premier exemple.

23
paradigmatic

La plus grande inefficacité lorsque vous écrivez un programme est de vous inquiéter des mauvaises choses. C'est généralement la mauvaise chose à craindre. Pourquoi?

  1. Le temps passé par le développeur coûte généralement beaucoup plus cher que le temps de calcul - en fait, il y a généralement une pénurie de la première et un excédent de la seconde.

  2. La plupart du code n'a pas besoin d'être très efficace car il ne s'exécutera jamais sur des jeux de données d'un million d'éléments plusieurs fois par seconde.

  3. La plupart du code doit être exempt de bogues, et moins de code laisse moins de place aux bogues.

17
Chuck

L'exemple que vous avez donné n'est pas très fonctionnel, en fait. Voici ce que vous faites:

// Given a list of Int
def removeMaxCool(xs: List[Int]): List[Int] = {

  // Find the index of the biggest Int
  val maxIndex = xs.indexOf(xs.max);

  // Then take the ints before and after it, and then concatenate then
  xs.take(maxIndex) ::: xs.drop(maxIndex+1)
}

Remarquez que ce n'est pas bad , mais vous savez quand le code fonctionnel est à son meilleur lorsqu'il décrit ce que vous voulez, au lieu de ce que vous voulez. Comme critique mineure, si vous utilisiez splitAt au lieu de take et drop, vous pourriez l’améliorer légèrement.

Une autre façon de le faire est la suivante:

def removeMaxCool(xs: List[Int]): List[Int] = {
  // the result is the folding of the tail over the head 
  // and an empty list
  xs.tail.foldLeft(xs.head -> List[Int]()) {

    // Where the accumulated list is increased by the
    // lesser of the current element and the accumulated
    // element, and the accumulated element is the maximum between them
    case ((max, ys), x) => 
      if (x > max) (x, max :: ys)
      else (max, x :: ys)

  // and of which we return only the accumulated list
  }._2
}

Parlons maintenant du problème principal. Ce code est-il plus lent que celui de Java? Certainement! Le code Java est-il plus lent qu'un équivalent C? Vous pouvez parier que c'est JIT ou pas JIT. Et si vous écrivez directement dans assembleur, vous pouvez le rendre encore plus rapidement!

Mais le coût de cette vitesse est que vous obtenez plus de bogues, vous passez plus de temps à essayer de comprendre le code pour le déboguer, et vous avez moins de visibilité sur le fonctionnement général du programme par rapport à ce que fait un petit morceau de code - - ce qui pourrait entraîner des problèmes de performance.

Ma réponse est donc simple: si vous pensez que la pénalité liée à la rapidité de la programmation en Scala ne vaut pas les gains qu’elle apporte, vous devriez programmer en assembleur. Si vous pensez que je suis radical, alors je vous réponds que vous venez de choisir le familier comme étant le compromis "idéal".

Est-ce que je pense que la performance n'a pas d'importance? Pas du tout! Je pense que l'un des principaux avantages de Scala est d'exploiter les gains que l'on trouve souvent dans les langages à typage dynamique avec les performances d'un langage à typage statique! La performance compte, la complexité des algorithmes, les coûts constants également.

Mais, chaque fois qu'il existe un choix entre performances et lisibilité et maintenabilité, cette dernière est préférable. Bien sûr, si les performances doivent être améliorées, il n’ya pas de choix: vous devez y sacrifier quelque chose. Et s'il n'y a pas de perte de lisibilité/maintenabilité - telle que Scala vs langages à typage dynamique -, optez pour la performance.

Enfin, pour tirer le meilleur parti de la programmation fonctionnelle, vous devez connaître les algorithmes fonctionnels et les structures de données. Certes, 99% des programmeurs Java avec 5 à 10 ans d’expérience battront les performances de 99% des programmeurs Scala avec 6 mois d’expérience. Il en était de même pour la programmation impérative par rapport à la programmation orientée objet il y a quelques décennies, et l'histoire montre que cela n'avait pas d'importance.

MODIFIER

En passant, votre algorithme "rapide" souffre d'un problème sérieux: vous utilisez ArrayBuffer. Cette collection n'a pas de temps constant, et a une durée linéaire toList. Si vous utilisez plutôt ListBuffer, vous obtiendrez une constante de temps qui ajoute et toList.

10
Daniel C. Sobral

Pour référence, voici comment splitAt est défini dans TraversableLike dans la bibliothèque standard Scala,

def splitAt(n: Int): (Repr, Repr) = {
  val l, r = newBuilder
  l.sizeHintBounded(n, this)
  if (n >= 0) r.sizeHint(this, -n)
  var i = 0
  for (x <- this) {
    (if (i < n) l else r) += x
    i += 1
  }
  (l.result, r.result)
}

Ce n'est pas différent de votre exemple de code de ce qu'un programmeur Java pourrait proposer.

J'aime Scala parce que, là où la performance compte, la mutabilité est une solution raisonnable. La bibliothèque de collections est un bon exemple. en particulier comment il cache cette mutabilité derrière une interface fonctionnelle. 

Lorsque les performances ne sont pas aussi importantes, comme certains codes d’application, les fonctions d’ordre supérieur de la bibliothèque de Scala permettent une grande expressivité et une efficacité accrue des programmeurs.


Par curiosité, j'ai choisi un fichier volumineux arbitraire dans le compilateur Scala ( scala.tools.nsc.typechecker.Typers.scala ) et j'ai compté quelque chose comme 37 boucles for, 11 boucles while, 6 concaténations (++) et 1 fois (il se trouve être une foldRight).

8
Kipton Barros

Et ça?

def removeMax(xs: List[Int]) = {
  val buf = xs.toBuffer
  buf -= (buf.max)
}

Un peu plus moche, mais plus rapide:

def removeMax(xs: List[Int]) = {
  var max = xs.head
  for ( x <- xs.tail ) 
  yield {
    if (x > max) { val result = max; max = x; result}
    else x
  }
}
4
soc

Essaye ça:

(myList.foldLeft((List[Int](), None: Option[Int]))) {
  case ((_, None),     x) => (List(),               Some(x))
  case ((Nil, Some(m), x) => (List(Math.min(x, m)), Some(Math.max(x, m))
  case ((l, Some(m),   x) => (Math.min(x, m) :: l,  Some(Math.max(x, m))
})._1

Idiomatique, fonctionnel, ne traverse qu'une fois. Peut-être un peu cryptique si vous n'êtes pas habitué aux idiomes de programmation fonctionnelle.

Essayons d'expliquer ce qui se passe ici. Je vais essayer de le rendre aussi simple que possible, sans rigueur.

Un fold est une opération sur un List[A] (c'est-à-dire une liste contenant des éléments de type A) qui prendra un état initial s0: S (c'est-à-dire une instance de type S) et une fonction f: (S, A) => S (c'est-à-dire , une fonction qui prend l’état actuel et un élément de la liste et donne l’état suivant, c’est-à-dire qu’elle met à jour l’état en fonction de l’élément suivant).

L'opération va ensuite parcourir les éléments de la liste, en utilisant chacun d'eux pour mettre à jour l'état en fonction de la fonction donnée. En Java, ce serait quelque chose comme:

interface Function<T, R> { R apply(T t); }
class Pair<A, B> { ... }
<State> State fold(List<A> list, State s0, Function<Pair<A, State>, State> f) {
  State s = s0;
  for (A a: list) {
    s = f.apply(new Pair<A, State>(a, s));
  }
  return s;
}

Par exemple, si vous souhaitez ajouter tous les éléments d'un List[Int], l'état serait la somme partielle, il faudrait l'initialiser à 0 et le nouvel état produit par une fonction ajouterait simplement l'état actuel à l'élément actuel. être en cours de traitement:

myList.fold(0)((partialSum, element) => partialSum + element)

Essayez d’écrire un pli pour multiplier les éléments d’une liste, puis un autre pour trouver les valeurs extrêmes (max, min).

Maintenant, le pli présenté ci-dessus est un peu plus complexe, car l’état est composé de la nouvelle liste créée avec le maximum d’éléments trouvé jusqu’à présent. La fonction qui met à jour l'état est plus ou moins simple une fois que vous avez compris ces concepts. Il met simplement dans la nouvelle liste le minimum entre le maximum actuel et l'élément actuel, tandis que l'autre valeur va au maximum actuel de l'état mis à jour.

Ce qui est un peu plus complexe que de comprendre cela (si vous n’avez pas de fond FP]) est de proposer cette solution. Cependant, ceci n'est que pour vous montrer que cela existe, que vous pouvez le faire. C'est juste un état d'esprit complètement différent.

EDIT: Comme vous le voyez, les première et deuxième case de la solution que j'ai proposée sont utilisées pour setup the fold. Cela équivaut à ce que vous voyez dans d’autres réponses quand elles fonctionnent xs.tail.fold((xs.head, ...)) {...}. Notez que les solutions proposées jusqu'à présent en utilisant xs.tail/xs.head ne couvrent pas le cas dans lequel xs est List(), et lèvera une exception. La solution ci-dessus renverra List() à la place. Puisque vous n'avez pas spécifié le comportement de la fonction sur des listes vides, les deux sont valides.

3
Bruno Reis

Une autre option serait:

package code.array

object SliceArrays {
  def main(args: Array[String]): Unit = {
    println(removeMaxCool(Vector(1,2,3,100,12,23,44)))
  }
  def removeMaxCool(xs: Vector[Int]) = xs.filter(_ < xs.max)
}

En utilisant Vector au lieu de List, la raison en est que Vector est plus polyvalent et offre une performance générale et une complexité temporelle meilleures par rapport à List.

Considérez les opérations de collections suivantes: head, tail, apply, update, prepend, append  

Vector prend un temps constant amorti pour toutes les opérations, comme indiqué dans Scala docs: "L’opération prend effectivement un temps constant, mais cela peut dépendre de certaines hypothèses telles que la longueur maximale d’un vecteur ou la distribution de clés de hachage"

La liste prend un temps constant uniquement pour les opérations en tête, en queue et en début de série.

En utilisant 

scalac - impression

génère:

package code.array {
  object SliceArrays extends Object {
    def main(args: Array[String]): Unit = scala.Predef.println(SliceArrays.this.removeMaxCool(scala.`package`.Vector().apply(scala.Predef.wrapIntArray(Array[Int]{1, 2, 3, 100, 12, 23, 44})).$asInstanceOf[scala.collection.immutable.Vector]()));
    def removeMaxCool(xs: scala.collection.immutable.Vector): scala.collection.immutable.Vector = xs.filter({
  ((x$1: Int) => SliceArrays.this.$anonfun$removeMaxCool$1(xs, x$1))
}).$asInstanceOf[scala.collection.immutable.Vector]();
    final <artifact> private[this] def $anonfun$removeMaxCool$1(xs$1: scala.collection.immutable.Vector, x$1: Int): Boolean = x$1.<(scala.Int.unbox(xs$1.max(scala.math.Ordering$Int)));
    def <init>(): code.array.SliceArrays.type = {
      SliceArrays.super.<init>();
      ()
    }
  }
}
0
guilhebl

Un autre concurrent. Ceci utilise un ListBuffer, comme la deuxième offre de Daniel, mais partage la queue post-max de la liste d'origine, en évitant de la copier.

  def shareTail(xs: List[Int]): List[Int] = {
    var res = ListBuffer[Int]()
    var maxTail = xs
    var first = true;
    var x = xs
    while ( x != Nil ) {
      if (x.head > maxTail.head) {
          while (!(maxTail.head == x.head)) {
              res += maxTail.head
              maxTail = maxTail.tail
          }
      }
      x = x.tail
    }
    res.prependToList(maxTail.tail)
  }
0
Ed Staub