Repository: numpy/numpy
Pull request: ENH: Enable huge pages in all Linux builds
This PR modifies memory allocation in such a way that huge pages for large arrays is enabled in all Linux builds (even if `MADV_HUGEPAGE` is not defined). As per discussion in #14177, this isn't the case currently, which means huge pages may never be available depending on the NumPy build, even if the runtime system supports it. Both conda-forge and pip builds for 1.17.0 seem to not include support for huge pages.

The `madvise()`  [man-pages](http://man7.org/linux/man-pages/man2/madvise.2.html) indicates that it will return `EINVAL` if the `advice` is invalid. At runtime, that means that it will be enabled if available, otherwise for the use case in NumPy it's enough to simply ignore the error to fallback to default behavior.

Fixes #14177 .

cc @hmaarrfk @jakirkham @charris @seberg @mrocklin 

Problem statement:
Improve large array allocation speed
I would like to be able to allocate NumPy arrays quickly.  Currently calling `numpy.ones` runs at about 2-3GB/s on consumer laptops (tested a 2018 Macbook pro and ThinkPad Carbon 4th gen with an i7)

My general intuition is that I should be able to allocate memory more quickly than this.  Is that true?  Is there anything that can be done here?

### Reproducing code example:

```python
import numpy

a = numpy.ones(shape=(1000000000), dtype='u1')  # 1 GB

# CPU times: user 293 ms, sys: 286 ms, total: 579 ms
# Wall time: 580 ms

>>> numpy.__version__
'1.17.0'
```

### Numpy/Python version information:

```
1.17.0 3.7.1 (default, Oct 23 2018, 14:07:42)
[Clang 4.0.1 (tags/RELEASE_401/final)]
```

Hints from issue discussion:
There are probably three things involved here: the OS, memory bandwidth (initialize to 1), and the numpy/python allocation implementation. How does the speed of empty/zeros compare? @juliantaylor would know more about this.
> How does the speed of empty/zeros compare? 

They're free to start, but we'll suffer the allocation costs eventually when we write to those pages

```python
In [1]: import numpy

In [2]: %%time
   ...: a = numpy.zeros(shape=(1000000000), dtype='u1')  # 1 GB
   ...:
   ...:
CPU times: user 16 µs, sys: 9 µs, total: 25 µs
Wall time: 29.8 µs

In [3]: %%time
   ...: a = numpy.empty(shape=(1000000000), dtype='u1')  # 1 GB
   ...:
   ...:
CPU times: user 17 µs, sys: 11 µs, total: 28 µs
Wall time: 33.1 µs
```
I get slightly faster times for the uninitialized allocations, however, `np.ones` is much faster
```
In [7]: %%time 
   ...: a = numpy.ones(shape=(1000000000), dtype='u1') 
   ...:  
   ...:                                                                         
CPU times: user 26 ms, sys: 55.5 ms, total: 81.4 ms
Wall time: 81.7 ms
```
This is on a six year old desktop `Intel(R) Core(TM) i5-4670K CPU @ 3.40GHz` with `1866MHz 2x8GB (16GB)`  memory. It seems that memory bandwidth is the determining factor here.
And perhaps compiler also, this is on fedora 30, gcc 9.1.1.
I would love it if I could get that performance :)

I'm now very curious what the determining factors are here.
I'd have to check in the BIOS to see how the memory is actually set up, I don't think DDR4 is supported, so am not sure any more what is actually in there. It may be overclocked a bit.

EDIT: G.SKILL · DDR3 · 8 GB · 1,866 MHz · 240-pin 
My guess it that it is a combination of compiler and laptop. IIRC, laptops tend to have slow memory. When I bought my current setup I think common laptops were running about 1/4 the speed. Power saving settings can also have a big impact.
I don't think that the memory frequency is the determining factor here. I'm on a desktop with `i7-7800X @3.50 GHz`, `1x16GB DDR4 @2400 MHz`, CPU governor set to `performance` and I am only seeing a bit faster results than @mrocklin:

```python
In [1]: import numpy as np

In [2]: np.__version__
Out[2]: '1.17.0'

In [3]: %%time
   ...: a = np.ones(int(1e9), dtype='u1')
   ...:
   ...:
CPU times: user 128 ms, sys: 208 ms, total: 336 ms
Wall time: 335 ms
```

I think it may have something to do with the compiler or some Linux configuration.

@charris if you watch `top`/`htop`/`free` do you see 1GB more of resident memory being allocated after `np.ones` get executed?
@pentschev Yes.
I wonder if it may have something to do with disk backup of the files in memory. I'm running two SSDs in RAID 0 (yes, I live dangerously :)

Commit message: ENH, REL: Enable huge pages in all Linux builds

Patch to review:
diff --git a/numpy/core/src/multiarray/alloc.c b/numpy/core/src/multiarray/alloc.c
index addc9f006683..e0d2d60563c3 100644
--- a/numpy/core/src/multiarray/alloc.c
+++ b/numpy/core/src/multiarray/alloc.c
@@ -25,11 +25,8 @@
 
 #include <assert.h>
 
-#ifdef HAVE_SYS_MMAN_H
+#ifdef NPY_OS_LINUX
 #include <sys/mman.h>
-#if defined MADV_HUGEPAGE && defined HAVE_MADVISE
-#define HAVE_MADV_HUGEPAGE
-#endif
 #endif
 
 #define NBUCKETS 1024 /* number of buckets for data*/
@@ -74,12 +71,19 @@ _npy_alloc_cache(npy_uintp nelem, npy_uintp esz, npy_uint msz,
 #ifdef _PyPyGC_AddMemoryPressure
         _PyPyPyGC_AddMemoryPressure(nelem * esz);
 #endif
-#ifdef HAVE_MADV_HUGEPAGE
+#ifdef NPY_OS_LINUX
         /* allow kernel allocating huge pages for large arrays */
         if (NPY_UNLIKELY(nelem * esz >= ((1u<<22u)))) {
             npy_uintp offset = 4096u - (npy_uintp)p % (4096u);
             npy_uintp length = nelem * esz - offset;
+#ifdef MADV_HUGEPAGE
             madvise((void*)((npy_uintp)p + offset), length, MADV_HUGEPAGE);
+#else
+            // Use code 14 (MADV_HUGEPAGE) if it isn't defined. This gives
+            // a chance of enabling huge pages even if build with linux
+            // kernel < 2.6.38
+            madvise((void*)((npy_uintp)p + offset), length, 14);
+#endif
         }
 #endif
     }


Code review comments:
diff_hunk: @@ -74,12 +71,19 @@ _npy_alloc_cache(npy_uintp nelem, npy_uintp esz, npy_uint msz,
 #ifdef _PyPyGC_AddMemoryPressure
         _PyPyPyGC_AddMemoryPressure(nelem * esz);
 #endif
-#ifdef HAVE_MADV_HUGEPAGE
+#ifdef NPY_OS_LINUX
         /* allow kernel allocating huge pages for large arrays */
         if (NPY_UNLIKELY(nelem * esz >= ((1u<<22u)))) {
             npy_uintp offset = 4096u - (npy_uintp)p % (4096u);
             npy_uintp length = nelem * esz - offset;
+#ifdef MADV_HUGEPAGE
             madvise((void*)((npy_uintp)p + offset), length, MADV_HUGEPAGE);
+#else
+            // Use code 14 (MADV_HUGEPAGE) if it isn't defined. This gives
+            // a chance of enabling huge pages even if build with linux
+            // kernel < 2.6.38
+            madvise((void*)((npy_uintp)p + offset), length, 14);
path: numpy/core/src/multiarray/alloc.c
text: @user1:
Why not just define MADV_HUGEPAGES and let it go through a more similar code path

@author:
Yes, thanks for the good idea, let me fix that.

@author:
Done in the latest commits, let me know if that's ok with you.

@user1:
ok with me means nothing, just a suggestion ;)

@author:
Well, if it's ok with you, it means that you probably don't disagree nor have an idea to make it even cleaner. :)