Chrome: как решить ошибки "Максимальный размер стека вызовов превышен" в Math.max.apply(Math, array)

Мне нужно найти максимальное и минимальное значение очень больших массивов. Для этого я использую

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

Это хорошо работает в Firefox и IE, но в Chrome я всегда получаю ошибки Maximum call stack size exceeded... мой текущий массив имеет 221954 элементов, и это не самое большое.

Кто-нибудь знает, как решить эту ошибку в Chrome? Как оптимизировать поиск значений max и min?

Для тех людей, которые не могут поверить, попробуйте это в консоли Chrome:

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

--- > RangeError: превышен максимальный размер стека вызовов

Ответ 1

Эта проблема не имеет ничего общего с Math.max и Math.min.

Функция .prototype.apply может принимать только массив ограниченной длины в качестве второго аргумента.

Локально, я протестировал его в Chrome, используя:

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

Локально, ограничение (l) разбилось точно с l = 124980. В канареере это было другое число, но также ~ 125k.

Это пример объяснения, почему это происходит: https://code.google.com/p/v8/issues/detail?id=2896 (он также может быть перепрограммирован в других JS-машинах, например, MDN имеет упоминание проблемы: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/apply#Using_apply_and_built-in_functions (Начиная с "Но будьте осторожны..." ), указывая на эту проблему в WebKit bugzilla: https://bugs.webkit.org/show_bug.cgi?id=80797). Насколько я понимаю, почему RangeError выбрасывается в V8:

V8 реализует функцию .prototype.apply в сборке. Перед вызовом функции он должен поместить все параметры вызова функции, например. thisArg и всех членов второго массива arg, один за другим, в стек, прежде чем вызывать функцию javascript. Но стек имеет ограниченную емкость, и если вы достигнете предела, вы получите RangeError.

Это то, что я нашел в источнике V8 (сборка 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);

Пожалуйста, проверьте! ADDED COMMENT для объяснения того, как я это понимаю.

И это функция APPLY_OVERFLOW, написанная в JS (опять же, источник V8, runtime.js):

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

EDIT: В вашем случае я бы хотел:

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

Ответ 2

Вы достигаете предела размера параметра функции. И ОК. Функция должна принимать только несколько параметров, так как еще есть запах кода.

Если у вас есть куча предметов? - Используйте массив. Вы используете .apply(), который проходит аргументов вроде: fun(1,2,3,4,5,6....) и достигает предела. Это плохая практика.

Проблема заключается в том, что Math.max() подключен для работы только так, поэтому наилучшим вариантом будет итеративная функция поиска. Но эта другая тема, так как производительность и алгоритм могут отличаться, например, если вы сначала отсортировали массив.

Ответ 3

Для меня ошибка не должна поступать из вызова в Math.min/max, это похоже на результат использования рекурсии, который я не могу исключить, поскольку Chrome будет использовать эти функции.

Встраиваются ли они в рекурсивный код?

Вы можете свернуть свой собственный минимальный/максимальный код тривиально, чтобы избежать проблемы в Chrome.

Ответ 4

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 может использовать рекурсивный метод для получения максимального значения, просто перепишите функцию итерации, чтобы получить max вместо. Это позволит избежать RangeError.