Repository: numba/numba

Issue:
Data for Numpy arrays are misaligned in LLVM IR
Hi,

While converting the LLVM IR generated by Numba to Javascript using Emscripten I came across a bug that seems to originate from the way Numba handles Numpy arrays. I use Numba to generate LLVM IR for some functions that used matrices. These matrices are stored in `np.array` instances. Now consider the following code:

``` python
from numba import njit, int32, double
import numpy as np

m = np.array([
    3.98, -98.765
])

@njit(double(int32))
def func(i):
    return m[i]/i
```

Here is the LLVM IR for it:

``` llvm
; ModuleID = 'func'
target datalayout = "e-m:e-p:32:32-i64:64-f80:32-n8:16:32-a:0:32-S32"
target triple = "i686-pc-windows-msvc"

@.const.picklebuf.87005624 = internal constant { i8*, i32 } { i8* getelementptr inbounds ([68 x i8], [68 x i8]* @.const.pickledata.87005624, i32 0, i32 0), i32 68 }
@.const.pickledata.87005624 = internal constant [68 x i8] c"\80\04\959\00\00\00\00\00\00\00\8C\08builtins\94\8C\11ZeroDivisionError\94\93\94\8C\10division by zero\94\85\94\86\94."
@.const.array.data = internal unnamed_addr constant [16 x i8] c"\D7\A3p=\0A\D7\0F@)\5C\8F\C2\F5\B0X\C0"

define double @func({ i8*, i32 }** noalias nocapture %excinfo, i32 %arg.i) {
entry:
  %.75 = icmp eq i32 %arg.i, 0
  br i1 %.75, label %B0.if, label %B0.endif

B0.if:                                            ; preds = %entry
  store { i8*, i32 }* @.const.picklebuf.87005624, { i8*, i32 }** %excinfo, align 4
  ret double -1.0

B0.endif:                                         ; preds = %entry
  %.49 = icmp slt i32 %arg.i, 0
  %.50 = add i32 %arg.i, 2
  %.51 = select i1 %.49, i32 %.50, i32 %arg.i
  %.64 = getelementptr double, double* bitcast ([16 x i8]* @.const.array.data to double*), i32 %.51
  %.65 = load double, double* %.64, align 8
  %.74 = sitofp i32 %arg.i to double
  %.79 = fdiv double %.65, %.74
  ret double %.79
}
```

Emscripten generated incorrect static data in JS for this LLVM IR and after [reporting it](https://github.com/kripken/emscripten/issues/4388) I learned that the data in the byte array is not aligned. The load instruction for the double (`%.65 = load double, double* %.64, align 8`) tries to use an alignment of 8 with a byte array which has an alignment of 1. Here is a direct quote: 

> My guess is this is an alignment issue. unnamed_addr constant [16 x i8] is just an array of bytes, with alignment 1. But the double loads declare alignment 8. That works on x86, but will fail on arm and asm.js, and is nonportable.
> 
> Try to make the alignment on the double loads 1, that could fix it. A better fix might be aligning the global constant.

Changing the alignment of the load call doesn't seem to solve my issue. How hard is it to have the generated byte array be aligned to the actual size of its elements (8 for `double`s in this case)? 



Discussion:
> Changing the alignment of the load call doesn't seem to solve my issue

This in itself sounds like an issue with Emscripten.

> How hard is it to have the generated byte array be aligned to the actual size of its elements

I think this is reasonably doable.

I completely agree, but still for me that means Numba will have to be modified to generate `load` instructions with correct alignment for the byte array.

For changing the alignment of the byte array, I might be able to implement this myself if you can point me to where I should look in the codebase. 

> For changing the alignment of the byte array, i might be able to implement this myself if you can point me to where I should look in the codebase. 

I think we first have to modify llvmlite to allow setting the alignment of a global variable.

Ok I'll dig into the code of llvmlite later today to see how easy that is to implement. 

See https://github.com/numba/llvmlite/pull/191 for the required llvmlite enhancement.

@pitrou Awesome man! Thanks for adding this to llvmlite since I still haven't had time to work on it. 


Resolving patch:
diff --git a/numba/targets/base.py b/numba/targets/base.py
--- a/numba/targets/base.py
+++ b/numba/targets/base.py
@@ -861,6 +861,7 @@ def make_constant_array(self, builder, typ, ary):
         A low-level contiguous array constant is created in the LLVM IR.
         """
         assert typ.layout == 'C'                # assumed in typeinfer.py
+        datatype = self.get_data_type(typ.dtype)
 
         # Handle data: reify the flattened array in "C" order as a
         # global array of bytes.
@@ -869,6 +870,8 @@ def make_constant_array(self, builder, typ, ary):
         #       workaround issue #1850 which is due to numpy issue #3147
         consts = Constant.array(Type.int(8), bytearray(flat.data))
         data = cgutils.global_constant(builder, ".const.array.data", consts)
+        # Ensure correct data alignment (issue #1933)
+        data.align = self.get_abi_alignment(datatype)
 
         # Handle shape
         llintp = self.get_value_type(types.intp)
@@ -905,10 +908,8 @@ def get_abi_sizeof(self, ty):
         """
         Get the ABI size of LLVM type *ty*.
         """
-        if isinstance(ty, llvmir.Type):
-            return ty.get_abi_size(self.target_data)
-        # XXX this one unused?
-        return self.target_data.get_abi_size(ty)
+        assert isinstance(ty, llvmir.Type), "Expected LLVM type"
+        return ty.get_abi_size(self.target_data)
 
     def get_abi_alignment(self, ty):
         """


Test patch:
diff --git a/numba/tests/test_arrayconst.py b/numba/tests/test_array_constants.py
similarity index 73%
rename from numba/tests/test_arrayconst.py
rename to numba/tests/test_array_constants.py
--- a/numba/tests/test_arrayconst.py
+++ b/numba/tests/test_array_constants.py
@@ -5,7 +5,7 @@
 import numba.unittest_support as unittest
 from numba.compiler import compile_isolated
 from numba.errors import TypingError
-from numba import types
+from numba import jit, types
 
 
 a0 = np.array(42)
@@ -21,6 +21,9 @@
 a4 = np.arange(32, dtype=np.int8).view(dt)
 a5 = a4[::-2]
 
+# A recognizable data string
+a6 = np.frombuffer(b"XXXX_array_contents_XXXX", dtype=np.float64)
+
 
 def getitem0(i):
     return a0[()]
@@ -46,6 +49,10 @@ def getitem5(i):
     return a5[i]
 
 
+def getitem6(i):
+    return a6[i]
+
+
 def use_arrayscalar_const():
     return s1
 
@@ -55,6 +62,9 @@ def write_to_global_array():
 
 
 class TestConstantArray(unittest.TestCase):
+    """
+    Test array constants.
+    """
 
     def check_array_const(self, pyfunc):
         cres = compile_isolated(pyfunc, (types.int32,))
@@ -80,6 +90,22 @@ def test_record_array_const_contig(self):
     def test_record_array_const_noncontig(self):
         self.check_array_const(getitem5)
 
+    def test_array_const_alignment(self):
+        """
+        Issue #1933: the array declaration in the LLVM IR must have
+        the right alignment specified.
+        """
+        sig = (types.intp,)
+        cfunc = jit(sig, nopython=True)(getitem6)
+        ir = cfunc.inspect_llvm(sig)
+        for line in ir.splitlines():
+            if 'XXXX_array_contents_XXXX' in line:
+                self.assertIn("constant [24 x i8]", line)  # sanity check
+                self.assertIn(", align 8", line)
+                break
+        else:
+            self.fail("could not find array declaration in LLVM IR")
+
     def test_arrayscalar_const(self):
         pyfunc = use_arrayscalar_const
         cres = compile_isolated(pyfunc, ())