Repository: numba/numba

Issue:
Integer to string conversion is extremely slow
<!--

Thanks for opening an issue! To help the Numba team handle your information
efficiently, please first ensure that there is no other issue present that
already describes the issue you have
(search at https://github.com/numba/numba/issues?&q=is%3Aissue).

-->

## Reporting a bug

<!--

Before submitting a bug report please ensure that you can check off these boxes:

-->

- [x] I have tried using the latest released version of Numba (most recent is
 visible in the change log (https://github.com/numba/numba/blob/master/CHANGE_LOG).
- [x] I have included below a minimal working reproducer (if you are unsure how
 to write one see http://matthewrocklin.com/blog/work/2018/02/28/minimal-bug-reports).

<!--

Please include details of the bug here, including, if applicable, what you
expected to happen!

-->

Integer to string conversion (`str(int)`) is extremely slow in Numba (~10x slower than Python and ~100x slower than C). The implementation ([here](https://github.com/numba/numba/blob/0db523ea8e0e5ad95d8a4262b4c9c44b65ecbc25/numba/cpython/unicode.py#L2353)) looks very inefficient since it allocates every single produced character. My guess is that someone copied it from CPython, but unlike Numba, CPython has string interning. The integer to string implementation can be much more efficient. Also, SSO (#3965) can avoid these allocations for many string programs in general.

```python
In [7]: def f(n):
   ...:     c = 0
   ...:     for i in range(n):
   ...:         c += len(str(n))
   ...:     return c
   ...:

In [8]: %timeit f(1000_000)
194 ms ± 3.17 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

In [10]: g = numba.njit(f)

In [11]: g(1000_000)
Out[11]: 7000000

In [12]: %timeit g(1000_000)
1.97 s ± 68.3 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
```


Discussion:
I can confirm the 10x speed difference.

Resolving patch:
diff --git a/numba/cpython/unicode.py b/numba/cpython/unicode.py
--- a/numba/cpython/unicode.py
+++ b/numba/cpython/unicode.py
@@ -2352,6 +2352,7 @@ def impl(i):
 @overload(str)
 def integer_str(n):
     if isinstance(n, types.Integer):
+
         ten = n(10)
 
         def impl(n):
@@ -2361,16 +2362,22 @@ def impl(n):
                 flag = True
             if n == 0:
                 return '0'
-            l = []
-            while n > 0:
-                c = chr(ord('0') + (n % ten))
-                n = n // ten
-                l.append(c)
+            length = flag + 1 + int(np.floor(np.log10(n)))
+            kind = PY_UNICODE_1BYTE_KIND
+            char_width = _kind_to_byte_width(kind)
+            s = _malloc_string(kind, char_width, length, True)
             if flag:
-                l.append('-')
-            return ''.join(l[::-1])
+                _set_code_point(s, 0, ord('-'))
+            idx = length - 1
+            while n > 0:
+                n, digit = divmod(n, ten)
+                c = ord('0') + digit
+                _set_code_point(s, idx, c)
+                idx -= 1
+            return s
         return impl
 
+
 # ------------------------------------------------------------------------------
 # iteration
 # ------------------------------------------------------------------------------


Test patch:
diff --git a/numba/tests/test_builtins.py b/numba/tests/test_builtins.py
--- a/numba/tests/test_builtins.py
+++ b/numba/tests/test_builtins.py
@@ -515,11 +515,14 @@ def test_int_str(self, flags=nrt_no_pyobj_flags):
             1234,
             1,
             0,
+            10,
+            1000,
         ]
 
         large_inputs = [
             123456789,
             2222222,
+            1000000,
             ~0x0
         ]