Почему 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
Ответы
Ответ 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