3

I run conda 4.6.3 with python 3.7.2 win32. In python, when I import numpy, i see the RAM usage increase by 80MB. Since I am using multiprocessing, I wonder if this is normal and if there is anyway to avoid this RAM overhead? Please see below all the versions from relevant packages (from conda list):

python...........3.7.2 h8c8aaf0_2

mkl_fft...........1.0.10 py37h14836fe_0

mkl_random..1.0.2 py37h343c172_0

numpy...........1.15.4 py37h19fb1c0_0

numpy-base..1.15.4 py37hc3f5095_0

thanks!

Community
  • 1
  • 1
dj K
  • 180
  • 2
  • 11
  • 1
    numpy is a ram hog, be sure to use `gc.collect()` to free up resources after doing a numpy operation. – tgikal Feb 13 '19 at 17:21
  • If you're short on ram, don't import the entire module but the few functions you are using. You might not save much but it can be enough. – Learning is a mess Feb 13 '19 at 17:31
  • 2
    @Learningisamess you can't do that. The whole module is always imported. – juanpa.arrivillaga Feb 13 '19 at 17:31
  • If this is python code there is nothing that alleviates the situation. For shared objects (DLLs or .so) your OS is responsible for optimising this. – deets Feb 13 '19 at 17:50
  • 2
    @tgikal: `gc.collect()` only helps if `numpy` is creating a bunch of reference cycles internally, not just allocating memory. CPython is reference counted, so as long as reference cycles don't occur, cleanup is pretty deterministic. The only scenario `gc.collect` would help in is if a cycle was created by accident (exceptions can create cycles involving the frames and their locals). `numpy` can also waste RAM since slicing arrays defaults to making new views, not copying, so a 1 KB slice can keep a 1 GB array alive after all other references are gone, but that's not related to the import cost. – ShadowRanger Feb 13 '19 at 18:37
  • 1
    @ShadowRanger That is true it will not have any effect on the import cost, I was merely referencing the tendency of numpy keeping arrays allocated after you're done using them. – tgikal Feb 13 '19 at 18:40

2 Answers2

2

You can't avoid this cost, but it's likely not as bad as it seems. The numpy libraries (a copy of C only libopenblasp, plus all the Python numpy extension modules) occupy over 60 MB on disk, and they're all going to be memory mapped into your Python process on import; adding on all the Python modules and the dynamically allocated memory involved in loading and initializing all of them, and 80 MB of increased reported RAM usage is pretty normal.

That said:

  1. The C libraries and Python extension modules are memory mapped in, but that doesn't actually mean they occupy "real" RAM; if the code paths in a given page aren't exercised, the page will either never be loaded, or will be dropped under memory pressure (not even written to the page file, since it can always reload it from the original DLL).
  2. On UNIX-like systems, when you fork (multiprocessing does this by default everywhere but Windows) that memory is shared between parent and worker processes in copy-on-write mode. Since the code itself is generally not written, the only cost is the page tables themselves (a tiny fraction of the memory they reference), and both parent and child will share that RAM.

Sadly, on Windows, fork isn't an option (unless you're running Ubuntu bash on Windows, in which case it's only barely Windows, effectively Linux), so you'll likely pay more of the memory costs in each process. But even there, libopenblasp, the C library backing large parts of numpy, will be remapped per process, but the OS should properly share that read-only memory across processes (and large parts, if not all, of the Python extension modules as well).

Basically, until this actually causes a problem (and it's unlikely to do so), don't worry about it.

ShadowRanger
  • 143,180
  • 12
  • 188
  • 271
  • thanks for your answer, I am using windows. So to make sure i understood you clearly, you're saying that the "Working Set (Memory)" column in the windows task manager can give an actual number that's higher than the actual real physical RAM that is used (especially if numpy is imported but never used by some processes)? If that's the case then i have no issue with this RAM usage as only a couple of my processes will really need numpy (but numpy just gets imported because one of my utils modules import numpy.) – dj K Feb 13 '19 at 21:20
  • @djkawa: Yeah. There are other task manager columns you can add (right click the column header, select `Select Columns`) that are more useful, e.g. `Memory (private working set)` and `Memory (shared working set)` (though I'm not sure I trust them to be completely accurate, it does show that not all of the working set is uniquely paid by a single process). The `numpy` import will inevitably involve some per-process costs, but large parts of it should be shared and/or not paged in in the first place (on my Windows 3.7.1 setup, just importing `numpy` only adds about 7 MB private, 2 MB shared). – ShadowRanger Feb 13 '19 at 21:53
  • interesting because on my machine, when i open two python process and just import numpy, all the memory columns roughly increase by the same amount (~75MB) for BOTH processes. Machine runs windows 7, python 3.7.2 and conda. i wonder if it's linked to conda environment hooks. Actually, I just tried with and without conda. and same increase on all memory columns for both processes. – dj K Feb 14 '19 at 22:10
  • @djkawa: As I said, I don't fully trust the objective accuracy of the Task Manager reports; my numbers are from Windows 10, and both the exact rules for computing it *and* the underlying behavior could easily have changed from Win7 to Win10. I'm not using `conda`, so I can't say if that has any effect; my setup is Windows 10, 64 bit Python 3.7.1 installed with the python.org installer, using `ipython` for my interactive interpreter. – ShadowRanger Feb 14 '19 at 22:37
1

[NumPy]: NumPy

is the fundamental package for scientific computing with Python.

It is a big package, designed to work with large datasets and optimized (primarily) for speed.
If you look in its __init__.py (which gets executed when importing it (e.g.: import numpy)), you'll notice that it imports lots of items (packages / modules):

  • Those items themselves, may import others
  • Some of them are extension modules (.pyds (.dlls) or .sos) which get loaded into the current process (their dependencies as well)

I've prepared a demo.

code.py:

#!/usr/bin/env python3

import sys
import os
import psutil
#import pprint


def main():
    display_text = "This {:s} screenshot was taken. Press <Enter> to continue ... "
    pid = os.getpid()
    print("Pid: {:d}\n".format(pid))
    p = psutil.Process(pid=pid)
    mod_names0 = set(k for k in sys.modules)
    mi0 = p.memory_info()

    input(display_text.format("first"))

    import numpy

    input(display_text.format("second"))

    mi1 = p.memory_info()
    for idx, mi in enumerate([mi0, mi1], start=1):
        print("\nMemory info ({:d}): {:}".format(idx, mi))

    print("\nExtra modules imported by `{:s}` :".format(numpy.__name__))
    print(sorted(set(k for k in sys.modules) - mod_names0))
    #pprint.pprint({k: v for k, v in sys.modules.items() if k not in mod_names0})
    print("\nDone.")


if __name__ == "__main__":
    print("Python {:s} on {:s}\n".format(sys.version, sys.platform))
    main()

Output:

[cfati@CFATI-5510-0:e:\Work\Dev\StackOverflow\q054675983]> "e:\Work\Dev\VEnvs\py_064_03.06.08_test0\Scripts\python.exe" code.py
Python 3.6.8 (tags/v3.6.8:3c6b436a57, Dec 24 2018, 00:16:47) [MSC v.1916 64 bit (AMD64)] on win32

Pid: 27160

This first screenshot was taken. Press <Enter> to continue ...
This second screenshot was taken. Press <Enter> to continue ...

Memory info (1): pmem(rss=15491072, vms=8458240, num_page_faults=4149, peak_wset=15495168, wset=15491072, peak_paged_pool=181160, paged_pool=180984, peak_nonpaged_pool=13720, nonpaged_pool=13576, pagefile=8458240, peak_pagefile=8458240, private=8458240)

Memory info (2): pmem(rss=27156480, vms=253882368, num_page_faults=7283, peak_wset=27205632, wset=27156480, peak_paged_pool=272160, paged_pool=272160, peak_nonpaged_pool=21640, nonpaged_pool=21056, pagefile=253882368, peak_pagefile=253972480, private=253882368)

Extra modules imported by `numpy` :
['_ast', '_bisect', '_blake2', '_compat_pickle', '_ctypes', '_decimal', '_hashlib', '_pickle', '_random', '_sha3', '_string', '_struct', 'argparse', 'ast', 'atexit', 'bisect', 'copy', 'ctypes', 'ctypes._endian', 'cython_runtime', 'decimal', 'difflib', 'gc', 'gettext', 'hashlib', 'logging', 'mtrand', 'numbers', 'numpy', 'numpy.__config__', 'numpy._distributor_init', 'numpy._globals', 'numpy._import_tools', 'numpy.add_newdocs', 'numpy.compat', 'numpy.compat._inspect', 'numpy.compat.py3k', 'numpy.core', 'numpy.core._internal', 'numpy.core._methods', 'numpy.core._multiarray_tests', 'numpy.core.arrayprint', 'numpy.core.defchararray', 'numpy.core.einsumfunc', 'numpy.core.fromnumeric', 'numpy.core.function_base', 'numpy.core.getlimits', 'numpy.core.info', 'numpy.core.machar', 'numpy.core.memmap', 'numpy.core.multiarray', 'numpy.core.numeric', 'numpy.core.numerictypes', 'numpy.core.records', 'numpy.core.shape_base', 'numpy.core.umath', 'numpy.ctypeslib', 'numpy.fft', 'numpy.fft.fftpack', 'numpy.fft.fftpack_lite', 'numpy.fft.helper', 'numpy.fft.info', 'numpy.lib', 'numpy.lib._datasource', 'numpy.lib._iotools', 'numpy.lib._version', 'numpy.lib.arraypad', 'numpy.lib.arraysetops', 'numpy.lib.arrayterator', 'numpy.lib.financial', 'numpy.lib.format', 'numpy.lib.function_base', 'numpy.lib.histograms', 'numpy.lib.index_tricks', 'numpy.lib.info', 'numpy.lib.mixins', 'numpy.lib.nanfunctions', 'numpy.lib.npyio', 'numpy.lib.polynomial', 'numpy.lib.scimath', 'numpy.lib.shape_base', 'numpy.lib.stride_tricks', 'numpy.lib.twodim_base', 'numpy.lib.type_check', 'numpy.lib.ufunclike', 'numpy.lib.utils', 'numpy.linalg', 'numpy.linalg._umath_linalg', 'numpy.linalg.info', 'numpy.linalg.lapack_lite', 'numpy.linalg.linalg', 'numpy.ma', 'numpy.ma.core', 'numpy.ma.extras', 'numpy.matrixlib', 'numpy.matrixlib.defmatrix', 'numpy.polynomial', 'numpy.polynomial._polybase', 'numpy.polynomial.chebyshev', 'numpy.polynomial.hermite', 'numpy.polynomial.hermite_e', 'numpy.polynomial.laguerre', 'numpy.polynomial.legendre', 'numpy.polynomial.polynomial', 'numpy.polynomial.polyutils', 'numpy.random', 'numpy.random.info', 'numpy.random.mtrand', 'numpy.testing', 'numpy.testing._private', 'numpy.testing._private.decorators', 'numpy.testing._private.nosetester', 'numpy.testing._private.pytesttester', 'numpy.testing._private.utils', 'numpy.version', 'pathlib', 'pickle', 'pprint', 'random', 'string', 'struct', 'tempfile', 'textwrap', 'unittest', 'unittest.case', 'unittest.loader', 'unittest.main', 'unittest.result', 'unittest.runner', 'unittest.signals', 'unittest.suite', 'unittest.util', 'urllib', 'urllib.parse']

Done.

And the (before and after import) screenshots ([MS.Docs]: Process Explorer):

Img0 - before

Img1 - after

As a personal remark, I think that ~80 MiB (or whatever the exact amount is), is more than decent for the current "era", which is characterized by ridiculously high amounts of hardware resources, especially in the memories area.
Besides, that would probably insignificant, compared to the amount required by the arrays themselves. If it's not the case, you should probably consider moving away from numpy.

There could be a way to reduce the memory footprint, by selectively importing only the modules containing the features that you need (my personal advice is against it), and thus going around __init__.py:

  • You'd have to be an expert in numpy's internals
  • Modules must be imported "manually" (by file name), using [Python 3]: importlib - The implementation of import (or alternatives)
  • Their dependents will be imported / loaded as well (and because of this, I don't know how much free memory you'd gain)
CristiFati
  • 38,250
  • 9
  • 50
  • 87
  • thanks for your answer, very useful to see all the modules imported by numpy. for the record, i am not saying 80MB is too much for numpy (i think numpy is great), but i was wondering if there was some kind of "lazy importing" options in numpy that would only load all the dependencies in memory when they were needed. but it seems that @ShadowRanger is pointing out that this memory is actually not physically used. In that case, my concern has no reason to be anymore. – dj K Feb 13 '19 at 21:26
  • The *.pyd*s are getting loaded instantly. their dependencies, also. *Nix* offers *RTLD\_LAZY*, while *Win* offers *delay loading*, but you can't control them (only for your *.dll*s). I wasn't talking as *80MB* strictly related to *numpy*, but in general. Any *OS* has its memory management system, and when the *RAM* tends to get full, it transfers the least unused (simplified version) pages to disk (*swap* or *Virtual Memory* (*pagefile.sys*)). – CristiFati Feb 13 '19 at 21:34