48

I've asked this question before about killing a process that uses too much memory, and I've got most of a solution worked out.

However, there is one problem: calculating massive numbers seems to be untouched by the method I'm trying to use. This code below is intended to put a 10 second CPU time limit on the process.

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)

What I expect to see when I run this script (on a Unix machine) is this:

-1 -1
ran out of time!

Instead, I get no output. The only way I get output is with Ctrl + C, and I get this if I Ctrl + C after 10 seconds:

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

If I Ctrl + C before 10 seconds, then I have to do it twice, and the console output looks like this:

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

In the course of experimenting and trying to figure this out, I've also put time.sleep(2) between the print and large number calculation. It doesn't seem to have any effect. If I change y = 10**(10**10) to y = 10**10, then the print and sleep statements work as expected. Adding flush=True to the print statement or sys.stdout.flush() after the print statement don't work either.

Why can I not limit CPU time for the calculation of a very large number? How can I fix or at least mitigate this?


Additional information:

Python version: 3.3.5 (default, Jul 22 2014, 18:16:02) \n[GCC 4.4.7 20120313 (Red Hat 4.4.7-4)]

Linux information: 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

Community
  • 1
  • 1
El'endia Starman
  • 2,204
  • 21
  • 35
  • 3
    I can reproduce on 3.4.3 but not 2.7.9. This is spooky. – senshin Dec 06 '15 at 03:23
  • Hmm...I always get `SystemError: PyEval_EvalFrameEx returned a result with an error set` whatever before or after 10 seconds on my `Arch Linux 4.2.5-1-ARCH` using Python 3.5. And if I use Python 2.7 run the code, it works fine as your expect output. – Remi Guan Dec 06 '15 at 03:23
  • 2
    I'm *guessing* that Python is trying to optimize by calculating constants beforehand...which backfires. But I have no proof of this. – El'endia Starman Dec 06 '15 at 03:23
  • 2
    @El'endiaStarman: Python *does* try to precalculate those constants even before execution. Write `def f(): return 10**(10**10)` in a module `proof.py`, and then try `import proof` and watch it eat all your CPU.. – DSM Dec 06 '15 at 03:29
  • (Related, though none of the answers are really satisfactory: http://stackoverflow.com/q/24217053/) – senshin Dec 06 '15 at 03:40
  • @DSM: Confirmed, all I have to do is write the function and I get the same hanging behavior, but all I get when I Ctrl+C is `^CKeyboardInterrupt`. – El'endia Starman Dec 06 '15 at 03:48
  • @DSM: Do you know of a place in the documentation or something where this precalculation of constants is discussed? I tried Googling, but I'm not turning up anything useful. – El'endia Starman Dec 06 '15 at 04:05

2 Answers2

51

TLDR: Python precomputes constants in the code. If any very large number is calculated with at least one intermediate step, the process will be CPU time limited.


It took quite a bit of searching, but I have discovered evidence that Python 3 does precompute constant literals that it finds in the code before evaluating anything. One of them is this webpage: A Peephole Optimizer for Python. I've quoted some of it below.

ConstantExpressionEvaluator

This class precomputes a number of constant expressions and stores them in the function's constants list, including obvious binary and unary operations and tuples consisting of just constants. Of particular note is the fact that complex literals are not represented by the compiler as constants but as expressions, so 2+3j appears as

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

This class converts those to

LOAD_CONST q (2+3j)

which can result in a fairly large performance boost for code that uses complex constants.

The fact that 2+3j is used as an example very strongly suggests that not only small constants are being precomputed and cached, but also any constant literals in the code. I also found this comment on another Stack Overflow question (Are constant computations cached in Python?):

Note that for Python 3, the peephole optimizer does precompute the 1/3 constant. (CPython specific, of course.) – Mark Dickinson Oct 7 at 19:40

These are supported by the fact that replacing

y = 10**(10**10)

with this also hangs, even though I never call the function!

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

The good news

Luckily for me, I don't have any such giant literal constants in my code. Any computation of such constants will happen later, which can be and is limited by the CPU time limit. I changed

y = 10**(10**10)

to this,

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

and got this output, as desired!

-1 -1
10
10000000000
ran out of time!

The moral of the story: Limiting a process by CPU time or memory consumption (or some other method) will work if there is not a large literal constant in the code that Python tries to precompute.

Community
  • 1
  • 1
El'endia Starman
  • 2,204
  • 21
  • 35
13

Use a function.

It does seem that Python tries to precompute integer literals (I only have empirical evidence; if anyone has a source please let me know). This would normally be a helpful optimization, since the vast majority of literals in scripts are probably small enough to not incur noticeable delays when precomputing. To get around this, you need to make your literal be the result of a non-constant computation, like a function call with parameters.

Example:

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()

This gives the expected result:

xubuntu@xubuntu-VirtualBox:~/Desktop$ time python3 hang.py
-1 -1
ran out of time!

real    0m10.027s
user    0m10.005s
sys     0m0.016s
  • This is an excellent observation! But note that it's not the `timeRanOut` handler that ends the process. It's the failure to calculate `f()` on time resulting in a `ValueError`, raised somewhere in Python's VM code. – 9000 Dec 06 '15 at 04:17
  • @9000 Unfortunately I'm not very familiar with the `resource` module. I believe it's throwing a `ValueError` because of the CPU resource limit being set to a value higher than my normal system limit. I'll experiment some more... –  Dec 06 '15 at 04:35
  • Interestingly, your code works entirely as expected (no `ValueError`) on my Ubuntu 14.04, both under Python 2.7 and 3.4. I suspect it has little to do with the Python module and more with limits set in your shell. – 9000 Dec 06 '15 at 04:48
  • @9000 That is almost certainly the case - I was running it under Cygwin in Windows, where a lot of things relating to POSIX and resource limits don't work properly (if at all). I blame not reading the tag. –  Dec 06 '15 at 04:50