Подтвердить что ты не робот

Почему Python "упреждающе" висит, пытаясь вычислить очень большое число?

Я задал этот вопрос до того, как убить процесс, который использует слишком много памяти, и у меня есть большая часть разработанного решения.

Однако есть одна проблема: вычисление массивных чисел, кажется, не затронуто методом, который я пытаюсь использовать. Этот код, приведенный ниже, предназначен для установки 10-секундного времени процессора на процесс.

import resource
import os
import signal

def timeRanOut(n, stack):
    raise SystemExit('ran out of time!')
signal.signal(signal.SIGXCPU, timeRanOut)

soft,hard = resource.getrlimit(resource.RLIMIT_CPU)
print(soft,hard)
resource.setrlimit(resource.RLIMIT_CPU, (10, 100))

y = 10**(10**10)

То, что я ожидаю увидеть при запуске этого script (на машине Unix), следующее:

-1 -1
ran out of time!

Вместо этого я не получаю никакого вывода. Единственный способ получить результат - это Ctrl + C, и я получаю это, если я Ctrl + C через 10 секунд:

^C-1 -1
ran out of time!
CPU time limit exceeded

Если я Ctrl + C до 10 секунд, то я должен сделать это дважды, а вывод консоли выглядит следующим образом:

^C-1 -1
^CTraceback (most recent call last):
  File "procLimitTest.py", line 18, in <module>
    y = 10**(10**10)
KeyboardInterrupt

В ходе экспериментов и попыток понять это, я также поставил time.sleep(2) между расчетом печати и большого числа. Кажется, это не имеет никакого эффекта. Если я изменю y = 10**(10**10) на y = 10**10, то операторы печати и сна будут работать должным образом. Добавление flush=True в оператор печати или sys.stdout.flush() после того, как инструкция печати также не работает.

Почему я не могу ограничить время процессора для вычисления очень большого числа? Как я могу исправить или хотя бы смягчить это?


Дополнительная информация:

Версия Python: 3.3.5 (default, Jul 22 2014, 18:16:02) \n[GCC 4.4.7 20120313 (Red Hat 4.4.7-4)]

Информация о Linux: Linux web455.webfaction.com 2.6.32-431.29.2.el6.x86_64 #1 SMP Tue Sep 9 21:36:05 UTC 2014 x86_64 x86_64 x86_64 GNU/Linux

4b9b3361

Ответ 1

TL;DR: Python предварительно вычисляет константы в коде. Если какое-либо очень большое число вычисляется по меньшей мере с одним промежуточным шагом, процесс будет ограниченным временем процессора.


Потребовалось довольно много поиска, но я обнаружил доказательства того, что Python 3 делает прекомпрессию постоянных литералов, которые он находит в коде, прежде чем оценивать что-либо. Одна из них - это веб-страница: Оптимизатор Peephole для Python. Я привел некоторые из них ниже.

ConstantExpressionEvaluator

Этот класс предварительно вычисляет ряд константных выражений и сохраняет их в списке констант функций, включая очевидные двоичные и унарные операции и кортежи, состоящие из простых констант. Особо следует отметить тот факт, что сложные литералы не представлены компилятором как константы, а как выражения, поэтому 2 + 3j появляется как

LOAD_CONST n (2) LOAD_CONST m (3j) BINARY_ADD

Этот класс преобразует их в

LOAD_CONST q (2+3j)

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

Тот факт, что 2+3j используется в качестве примера, очень сильно предполагает, что не только небольшие константы предварительно вычисляются и кэшируются, но и любые константные литералы в коде. Я также нашел этот комментарий в другом вопросе о переполнении (Являются ли постоянные вычисления кэшированными в Python?):

Обратите внимание, что для Python 3 оптимизатор глазок прекомпретирует константу 1/3. (Конкретный CPython, конечно.) - Марк Дикинсон 7 октября в 19:40

Это подтверждается тем фактом, что замена

y = 10**(10**10)

с этим также зависает, хотя я никогда не вызываю функцию!

def f():
    y = 10**(10**10)

Хорошие новости

К счастью для меня, у меня нет таких гигантских литералов в моем коде. Любое вычисление таких констант произойдет позже, что может быть ограничено временным пределом ЦП. Я изменил

y = 10**(10**10)

x = 10
print(x)
y = 10**x
print(y)
z = 10**y
print(z)

и получил этот выход по желанию!

-1 -1
10
10000000000
ran out of time!

Мораль истории: Ограничение процесса по времени процессора или потреблению памяти (или некоторому другому методу) будет работать, если в коде, который Python пытается прекомпопутировать, нет большой константы литерала.

Ответ 2

Используйте функцию.

Кажется, что Python пытается прекомпопировать целочисленные литералы (у меня есть только эмпирические данные, если у кого-то есть источник, пожалуйста, дайте мне знать). Обычно это будет полезной оптимизацией, поскольку подавляющее большинство литералов в сценариях, вероятно, достаточно малы, чтобы не вызывать заметных задержек при предварительном вычислении. Чтобы обойти это, вам нужно сделать ваш литерал результатом не-постоянных вычислений, таких как вызов функции с параметрами.

Пример:

import resource
import os
import signal

def timeRanOut(n, stack):
    raise SystemExit('ran out of time!')
signal.signal(signal.SIGXCPU, timeRanOut)

soft,hard = resource.getrlimit(resource.RLIMIT_CPU)
print(soft,hard)
resource.setrlimit(resource.RLIMIT_CPU, (10, 100))

f = lambda x=10:x**(x**x)
y = f()

Это дает ожидаемый результат:

[email protected]:~/Desktop$ time python3 hang.py
-1 -1
ran out of time!

real    0m10.027s
user    0m10.005s
sys     0m0.016s