0

.

Hi.

I'm trying to develop a postfix calculator in Cython, translated from a working Numpy version. It is my first attempt. The calculator function gets the postfix expression in a list and the samples matrix. Then, it must return the calculated array.

Input example:

postfix = ['X0', 'X1', 'add']
samples = [[0, 1], 
           [2, 3], 
           [4, 5]]
result = [1, 5, 9]

example_cython.pyx

#cython: boundscheck=False, wraparound=False, nonecheck=False

import numpy
from libc.math cimport sin as c_sin

cdef inline calculate(list lst, double [:,:] samples):
    cdef int N = samples.shape[0]
    cdef int i, j
    cdef list stack = []
    cdef double[:] Y = numpy.zeros(N)

    for p in lst:
        if p == 'add':
            b = stack.pop()
            a = stack.pop()
            for i in range(N):
                Y[i] = a[i] + b[i]
            stack.append(Y)
        elif p == 'sub':
            b = stack.pop()
            a = stack.pop()
            for i in range(N):
                Y[i] = a[i] - b[i]
            stack.append(Y)
        elif p == 'mul':
            b = stack.pop()
            a = stack.pop()
            for i in range(N):
                Y[i] = a[i] * b[i]
            stack.append(Y)
        elif p == 'div':
            b = stack.pop()
            a = stack.pop()
            for i in range(N):
                if abs(b[i]) < 1e-4: b[i]=1e-4
                Y[i] = a[i] / b[i]
            stack.append(Y)
        elif p == 'sin':
            a = stack.pop()
            for i in range(N):
                Y[i] = c_sin(a[i])
            stack.append(Y)
        else:
            if p[0] == 'X':
                j = int(p[1:])
                stack.append (samples[:, j])
            else:
                stack.append(float(p))
    return stack.pop ()


# Generate and evaluate expressions
cpdef test3(double [:,:] samples, object _opchars, object _inputs, int nExpr):
    for i in range(nExpr):
        size = 2
        postfix = list(numpy.concatenate((numpy.random.choice(_inputs, 5*size),
                                        numpy.random.choice(_inputs + _opchars, size),
                                        numpy.random.choice(_opchars, size)), 0))
        #print postfix

        res = calculate(postfix, samples)

main.py

import random
import time
import numpy
from example_cython import test3

# Random dataset
n = 1030
nDim=10
samples = numpy.random.uniform(size=(n, nDim))

_inputs = ['X'+str(i) for i in range(nDim)]
_ops_1 = ['sin']
_ops_2 = ['add', 'sub', 'mul', 'div']
_opchars = _ops_1 + _ops_2
nExpr = 1000
nTrials = 3

tic = time.time ()
for i in range(nTrials): test3(samples, _opchars, _inputs, nExpr)
print ("TEST 1: It took an average of {} seconds to evaluate {} expressions on a dataset of {} rows and {} columns.".format(str((time.time () - tic)/nTrials), str(nExpr), str(n), str(nDim)))

setup.py

from distutils.core import setup
from distutils.extension import Extension
from Cython.Distutils import build_ext

ext_modules=[ Extension("example_cython",
              ["example_cython.pyx"],
              libraries=["m"],
              extra_compile_args = ["-Ofast", "-ffast-math"])]

setup(
  name = "example_cython",
  cmdclass = {"build_ext": build_ext},
  ext_modules = ext_modules)

Configuration:

Python 3.6.2 |Anaconda, Inc.| (default, Sep 21 2017, 18:29:43)
[GCC 4.2.1 Compatible Clang 4.0.1 (tags/RELEASE_401/final)] on darwin

>>> numpy.__version__
'1.13.1'
>>> cython.__version__
'0.26.1'

Compilation and run:

running build_ext
skipping 'example_cython.c' Cython extension (up-to-date)
building 'example_cython' extension
/usr/bin/clang -Wno-unused-result -Wsign-compare -Wunreachable-code -DNDEBUG -fwrapv -O2 -Wall -Wstrict-prototypes -march=core2 -mtune=haswell -mssse3 -ftree-vectorize -fPIC -fPIE -fstack-protector-strong -O2 -pipe -march=core2 -mtune=haswell -mssse3 -ftree-vectorize -fPIC -fPIE -fstack-protector-strong -O2 -pipe -I/Users/vmelo/anaconda3/include/python3.6m -c example_cython.c -o build/temp.macosx-10.9-x86_64-3.6/example_cython.o -Ofast -ffast-math
example_cython.c:2506:15: warning: code will never be executed [-Wunreachable-code]
    if (0 && (__pyx_tmp_idx < 0 || __pyx_tmp_idx >= __pyx_tmp_shape)) {
              ^~~~~~~~~~~~~
example_cython.c:2506:9: note: silence by adding parentheses to mark code as explicitly dead
    if (0 && (__pyx_tmp_idx < 0 || __pyx_tmp_idx >= __pyx_tmp_shape)) {
        ^
        /* DISABLES CODE */ ( )
example_cython.c:2505:9: warning: code will never be executed [-Wunreachable-code]
        __pyx_tmp_idx += __pyx_tmp_shape;
        ^~~~~~~~~~~~~
example_cython.c:2504:9: note: silence by adding parentheses to mark code as explicitly dead
    if (0 && (__pyx_tmp_idx < 0))
        ^
        /* DISABLES CODE */ ( )
2 warnings generated.
/usr/bin/clang -bundle -undefined dynamic_lookup -Wl,-pie -Wl,-headerpad_max_install_names -Wl,-rpath,/Users/vmelo/anaconda3/lib -L/Users/vmelo/anaconda3/lib -Wl,-pie -Wl,-headerpad_max_install_names -Wl,-rpath,/Users/vmelo/anaconda3/lib -L/Users/vmelo/anaconda3/lib -arch x86_64 build/temp.macosx-10.9-x86_64-3.6/example_cython.o -L/Users/vmelo/anaconda3/lib -lm -o /Users/vmelo/Dropbox/SRC/python/random_equation/cython_v2/example_cython.cpython-36m-darwin.so
ld: warning: -pie being ignored. It is only used when linking a main executable

TEST 1: It took an average of 1.2609198093414307 seconds to evaluate 1000 expressions on a dataset of 1030 rows and 10 columns.

It takes around 1,25 seconds to run on my i5 1.4Ghz. However, a similar C code takes 0,13 seconds.

The above code evaluates 1000 expressions, but I'm aiming at 1,000,000. Thus, I must speed-up this Cython code by a large margin.

As I wrote at the beginning, the Numpy version is working properly. Maybe, in this Cython version, I shouldn't use a list as a stack? I'm still not checking if the results generated by this Cython code are correct, as I'm focused on improving its speed.

Any suggestions?

Thanks.

user1348438
  • 71
  • 1
  • 4

1 Answers1

1

Currently the only operation that's optimised is indexing samples[:, j]. (Almost) everything else is untyped and so Cython can't optimise it much.

I don't really want to rewrite your (fairly large) program completely, but here's some simple ideas on how to improve it.

  1. Fix a basic logic error - you need the line Y = numpy.zeros(N) inside your loop. stack.append(Y) does not make a copy of Y, so every time you modify Y you also modify all the other versions you've put on the stack.

  2. Set a type for a and b:

    cdef double[:] a, b # at the start of the program
    

    This will significantly speed up the indexing of

    Y[i] = a[i] * b[i]
    

    however, it will cause the line like a = stack.pop() to be a little slower as it needs to check you that the result can be used as a memoryview. You will also need to change the line

    stack.append(float(p))
    

    to ensure you put something usable with a memoryview on the stack:

    stack.append(float(p)*np.ones(N))
    
  3. Change the stack to a 2D memoryview. I'd suggest you over-allocate it and just keep a count of number_on_stack and then reallocate the stack if needed. You can then change:

    stack.append(samples[:, j])
    

    to:

    if stack.shape[1] < number_on_stack+1:
        # resize numpy array
        arr = stack.base
        arr.resize(... larger shape ..., refcheck=False)
        stack = arr # re-link stack to resized array (to ensure size is suitably updated)
    stack[:,number_on_stack+1] = samples[:,j]
    number_on_stack += 1
    

    and

    a = stack.pop()
    

    to

    a = stack[:,number_on_stack]
    number_on_stack -= 1
    

    Other changes follow a similar pattern. This option is most work, but likely to get best results.


Using cython -a to generate coloured HTML gives you a reasonable idea of which bits are well optimised (yellow code is generally worse)

DavidW
  • 29,336
  • 6
  • 55
  • 86