web-dev-qa-db-fra.com

Chrome: Comment résoudre les erreurs "Taille maximale de la pile d'appels dépassée" sur Math.max.apply (Math, array)

Je dois trouver la valeur maximale et minimale de très grands tableaux. Pour cela j'utilise

Math.max.apply(Math, my_array);
Math.min.apply(Math, my_array);

Cela fonctionne bien sur Firefox et IE, mais sur Chrome, je reçois toujours des erreurs Maximum call stack size exceeded ... mon tableau actuel contient 221954 éléments, et ce n'est pas mon plus gros problème.

Est-ce que quelqu'un sait comment résoudre cette erreur sur Chrome? Comment puis-je optimiser la recherche des valeurs max et min?

Pour ceux qui ne peuvent pas croire, essayez ceci dans la console de Chrome:

var xxx = []
for(var i=0; i<300000; i++){
    xxx.Push(Math.random());
}
Math.max.apply(Math, xxx);

---> RangeError: taille maximale de la pile d'appels dépassée

12
PanChan

Ce problème n'a rien à voir spécifiquement avec Math.max et Math.min.

Function.prototype.apply ne peut recevoir qu'un tableau de longueur limitée en tant que deuxième argument.

Localement, je l'ai testé sous Chrome en utilisant:

function limit(l) {
  var x = []; x.length = l;
  (function (){}).apply(null, x);
}

Localement, limite (l) s’est écrasée exactement avec l = 124980. Dans le canari, c’était un autre nombre, mais aussi ~ 125k.

Voici un exemple d’explication: https://code.google.com/p/v8/issues/detail?id=2896 (il est également reproductible dans d’autres moteurs JS, par exemple, MDN a une mention du problème: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/apply#Using_apply_and_built-in_functions (à partir de "mais soyez attentif ..." ), soulignant ce problème dans WebKit bugzilla: https://bugs.webkit.org/show_bug.cgi?id=80797 ). Autant que je sache pourquoi RangeError est lancé dans la V8:

La V8 implémente Function.prototype.apply dans Assembly. Avant d’appeler la fonction, tous les paramètres d’appel de cette fonction doivent être placés, p. Ex. thisArg et tous les membres du 2nd arg array, un par un}, sur la pile avant d'appeler la fonction javascript. Mais la pile a une capacité limitée et si vous atteignez la limite, vous obtiendrez RangeError.

Voici ce que j'ai trouvé dans le source V8 (assemblage IA-32, builtins-ia32.cc):

void Builtins::Generate_FunctionApply(MacroAssembler* masm) {
  static const int kArgumentsOffset = 2 * kPointerSize;
  static const int kReceiverOffset = 3 * kPointerSize;
  static const int kFunctionOffset = 4 * kPointerSize;
  {
    FrameScope frame_scope(masm, StackFrame::INTERNAL);

    __ Push(Operand(ebp, kFunctionOffset));  // Push this
    __ Push(Operand(ebp, kArgumentsOffset));  // Push arguments
    __ InvokeBuiltin(Builtins::APPLY_PREPARE, CALL_FUNCTION);

    // Check the stack for overflow. We are not trying to catch
    // interruptions (e.g. debug break and preemption) here, so the "real stack
    // limit" is checked.
    Label okay;
    ExternalReference real_stack_limit =
        ExternalReference::address_of_real_stack_limit(masm->isolate());
    __ mov(edi, Operand::StaticVariable(real_stack_limit));
    // Make ecx the space we have left. The stack might already be overflowed
    // here which will cause ecx to become negative.
    // !! ADDED COMMENT: IA-32 stack grows downwards, if address to its current top is 0 then it cannot be placed any more elements into. esp is the pointer to stack top.
    __ mov(ecx, esp);
    // !! ADDED COMMENT: edi holds the "real_stack_limit", which holds the  minimum address that stack should not grow beyond. If we subtract edi from ecx (=esp, or, in other words, "how much space is left on the stack"), we may get a negative value, and the comment above says that
    __ sub(ecx, edi);
    // Make edx the space we need for the array when it is unrolled onto the
    // stack.
    // !! ADDED COMMENT: eax holds the number of arguments for this apply call, where every member of the 2nd argument array counts as separate argument
    __ mov(edx, eax);
    // !! ADDED COMMENT: kPointerSizeLog2 - kSmiTagSize is the base-2-logarithm of how much space would 1 argument take. By shl we in fact get 2^(kPointerSizeLog2 - kSmiTagSize) * arguments_count, i.e. how much space do actual arguments occupy
    __ shl(edx, kPointerSizeLog2 - kSmiTagSize);
    // Check if the arguments will overflow the stack.
    // !! ADDED COMMENT: we compare ecx which is how much data we can put onto stack with edx which now means how much data we need to put onto stack
    __ cmp(ecx, edx);
    __ j(greater, &okay);  // Signed comparison.

    // Out of stack space.
    __ Push(Operand(ebp, 4 * kPointerSize));  // Push this
    __ Push(eax);
    __ InvokeBuiltin(Builtins::APPLY_OVERFLOW, CALL_FUNCTION);

Vérifiez s'il vous plaît !! AJOUT DE COMMENTAIRE pour des explications sur la façon dont je le comprends.

Et ceci est la fonction APPLY_OVERFLOW, écrite en JS (encore une fois, source V8, runtime.js):

function APPLY_OVERFLOW(length) {
  throw %MakeRangeError('stack_overflow', []);
}

EDIT: Dans votre cas, j'irais comme:

var max = -Infinity; 
for(var i = 0; i < arr.length; i++ ) if (arr[i] > max) max = arr[i];
19
t.O

Vous atteignez la limite de taille de paramètre de fonction. Et c’estOK. La fonction n’accepte que quelques paramètres, sinon c’est un code odeur .

Si vous avez un tas d'articles? - Utilisez un tableau. Vous utilisez .apply() qui transmet des arguments Tels que: fun(1,2,3,4,5,6....) et atteint la limite. C'est une mauvaise pratique .

Le problème est que - Math.max() est câblé pour fonctionner uniquement de la sorte, votre meilleur choix serait donc la fonction de recherche itérative. Mais c’est un autre sujet, car les performances et l’algorithme peuvent varier, par exemple, si vous triez d’abord le tableau.

1
lukas.pukenis

Pour moi, l'erreur ne devrait pas provenir de l'appel dans Math.min/max. Cela ressemble au résultat de l'utilisation de la récursivité, ce que je ne peux pas croire que Chrome utiliserait pour implémenter ces fonctions.

Sont-ils intégrés dans du code récursif?

Vous pouvez lancer votre propre code min/max de manière triviale pour éviter le problème dans Chrome.

0
Tom
var a=[]; 
for(var i=0;i<1125011;i++){ 
    a[i] = i;
}
function maxIterate(arr){
    var max = arr[0];
    for(var i = 1;i< arr.length; i++){
        (max < arr[i]) && (max = arr[i])
    }
    return max;
}
console.log(maxIterate(a));

Math.max peut utiliser une méthode récursive pour obtenir la valeur maximale; il suffit de réécrire une fonction itérative pour obtenir la valeur maximale à la place. Cela évitera RangeError.

0
Fly_pig