Repository: numba/numba

Issue:
UnicodeView type
As an optimization, we could create a special `UnicodeViewType` which is functionally identical to a `UnicodeType`, but is a zero-copy view into the storage of another `UnicodeType`.  This would make slicing a zero allocation operation, and also speed up `str.split()` a lot (see #3678 for some discussion there).

Some notes:

* The behavior here would be similar to NumPy, where the view holds a reference count on a buffer object that it may or may not own.  This means a small view of a long string might force the entire string to stick around, increasing memory usage.
* `UnicodeViewType` would have no unboxing method, and boxing would need to copy the data to Python storage (as `UnicodeType` already does), but also decref the underlying string buffer.


Discussion:
@seibert I just started looking at this. What about using the same type and not creating a new UnicodeViewType? Looks like UnicodeType doesn't actually depend on owning the data buffer anywhere. The only place I could think of is boxing, but `PyUnicode_FromKindAndData` copies the data anyways (python docs is a bit unclear to me but the code which copes is [here](https://github.com/python/cpython/blob/master/Objects/unicodeobject.c#L2208))
The main issue is properly reference counting the thing that owns the buffer, which could either have been allocated by Python or by Numba.  I guess if UnicodeType could retain a reference to either a Python object or another Numba UnicodeType value, a small tweak to unboxing would ensure the right thing happens.
Ok, I'll work in this direction and submit a WIP PR for discussion. Thanks!

Resolving patch:
diff --git a/numba/unicode.py b/numba/unicode.py
--- a/numba/unicode.py
+++ b/numba/unicode.py
@@ -702,6 +702,43 @@ def _strncpy(dst, dst_offset, src, src_offset, n):
                             _get_code_point(src, src_offset + i))
 
 
+@intrinsic
+def _get_str_slice_view(typingctx, src_t, start_t, length_t):
+    """Create a slice of a unicode string using a view of its data to avoid
+    extra allocation.
+    """
+    assert src_t == types.unicode_type
+
+    def codegen(context, builder, sig, args):
+        src, start, length = args
+        in_str = cgutils.create_struct_proxy(
+            types.unicode_type)(context, builder, value=src)
+        view_str = cgutils.create_struct_proxy(
+            types.unicode_type)(context, builder)
+        view_str.meminfo = in_str.meminfo
+        view_str.kind = in_str.kind
+        view_str.length = length
+        # hash value -1 to indicate "need to compute hash"
+        view_str.hash = context.get_constant(_Py_hash_t, -1)
+        # get a pointer to start of slice data
+        bw_typ = context.typing_context.resolve_value_type(_kind_to_byte_width)
+        bw_sig = bw_typ.get_call_type(
+            context.typing_context, (types.int32,), {})
+        bw_impl = context.get_function(bw_typ, bw_sig)
+        byte_width = bw_impl(builder, (in_str.kind,))
+        offset = builder.mul(start, byte_width)
+        view_str.data = builder.gep(in_str.data, [offset])
+        # Set parent pyobject to NULL
+        view_str.parent = cgutils.get_null_value(view_str.parent.type)
+        # incref original string
+        if context.enable_nrt:
+            context.nrt.incref(builder, sig.args[0], src)
+        return view_str._getvalue()
+
+    sig = types.unicode_type(types.unicode_type, types.intp, types.intp)
+    return sig, codegen
+
+
 @overload(operator.getitem)
 def unicode_getitem(s, idx):
     if isinstance(s, types.UnicodeType):
@@ -716,16 +753,16 @@ def getitem_char(s, idx):
             def getitem_slice(s, idx):
                 slice_idx = _normalize_slice(idx, len(s))
                 span = _slice_span(slice_idx)
-                ret = _empty_string(s._kind, span)
 
                 if slice_idx.step == 1:
-                    _strncpy(ret, 0, s, slice_idx.start, span)
+                    return _get_str_slice_view(s, slice_idx.start, span)
                 else:
+                    ret = _empty_string(s._kind, span)
                     cur = slice_idx.start
                     for i in range(span):
                         _set_code_point(ret, i, _get_code_point(s, cur))
                         cur += slice_idx.step
-                return ret
+                    return ret
             return getitem_slice
 
 


Test patch: