Время разыменования указателя функции Cython (по сравнению с прямым вызовом функции) - PullRequest
0 голосов
/ 03 марта 2019

У меня есть некоторый код Cython, включающий чрезвычайно повторяющиеся операции по пикселям для массивов Numpy (представляющих изображения BGR) следующей формы:

ctypedef double (*blend_type)(double, double) # function pointer
@cython.boundscheck(False)  # Deactivate bounds checking
@cython.wraparound(False)   # Deactivate negative indexing.
cdef cnp.ndarray[cnp.float_t, ndim=3] blend_it(const double[:, :, :] array_1, const double[:, :, :] array_2, const blend_type blendfunc, const double opacity):
  # the base layer is a (array_1)
  # the blend layer is b (array_2)
  # base layer is below blend layer
  cdef Py_ssize_t y_len = array_1.shape[0]
  cdef Py_ssize_t x_len = array_1.shape[1]
  cdef Py_ssize_t a_channels = array_1.shape[2]
  cdef Py_ssize_t b_channels = array_2.shape[2]
  cdef cnp.ndarray[cnp.float_t, ndim=3] result = np.zeros((y_len, x_len, a_channels), dtype = np.float_)
  cdef double[:, :, :] result_view = result
  cdef Py_ssize_t x, y, c

  for y in range(y_len):
    for x in range(x_len):
      for c in range(3): # iterate over BGR channels first
        # calculate channel values via blend mode
        a = array_1[y, x, c]
        b = array_2[y, x, c]
        result_view[y, x, c] = blendfunc(a, b)
        # many other operations involving result_view...
  return result;

Где blendfunc относится к другой функции Cython, такой какследующий overlay_pix:

cdef double overlay_pix(double a, double b):
  if a < 0.5:
    return 2*a*b
  else:
    return 1 - 2*(1 - a)*(1 - b)

Цель использования указателя функции состоит в том, чтобы избежать необходимости переписывать этот огромный беспорядок повторяющегося кода снова и снова для каждого режима смешивания (которых существует множество).Поэтому я создал такой интерфейс для каждого режима наложения, избавив меня от этой проблемы:

def overlay(double[:, :, :] array_1, double[:, :, :] array_2, double opacity = 1.0):
  return blend_it(array_1, array_2, overlay_pix, opacity)

Однако, похоже, это стоит мне времени!Я заметил, что для очень больших изображений (таких как изображения 8K и более), существует значительная потеря времени при использовании blendfunc в функции blend_it вместо прямого вызова overlay_pix в blend_it.Я предполагаю, что это потому, что blend_it должен разыменовывать указатель функции каждый раз в итерации вместо того, чтобы иметь функцию, немедленно доступную для него, но я точно не знаю.

Потеря времени не идеальна, но я, конечно, не хочу переписывать blend_it снова и снова для каждого режима наложения.Есть ли способ избежать потери времени?Есть ли способ превратить указатель функции в локальную функцию вне цикла, а затем быстрее получить к ней доступ внутри цикла?

Ответы [ 2 ]

0 голосов
/ 03 марта 2019

@ ead's answer говорит о двух интересных вещах:

  1. C может оптимизировать его для прямого вызова.Я не думаю, что это, как правило, верно, за исключением довольно простых случаев, и не похоже, что это верно для компиляторов и кода, который использует OP.
  2. В C ++ вместо этого вы используете шаблоны - это определенноtrue и поскольку типы шаблонов всегда известны во время компиляции, оптимизация обычно проста.

Шаблоны Cython и C ++ немного беспорядочные, поэтому я не думаю, что вы хотите использовать их здесь,Однако у Cython есть шаблоноподобная функция, называемая слитыми типами .Вы можете использовать слитые типы для оптимизации во время компиляции, как показано ниже.Примерное описание кода:

  1. Определите cdef class, содержащую функцию staticmethod cdef для всех операций, которые вы хотите выполнить.
  2. Определите тип слитного кодасодержащий все cdef class о.(Это ограничение этого подхода - его нелегко расширить, поэтому, если вы хотите добавить операции, вам нужно отредактировать код)
  3. Определите функцию, которая принимает фиктивный аргумент вашего слитого типа.Используйте этот тип для вызова staticmethod.
  4. Определить функции-оболочки - вам нужно использовать явный синтаксис [type], чтобы заставить его работать.

Код:

import cython

cdef class Plus:
    @staticmethod
    cdef double func(double x):
        return x+1    

cdef class Minus:
    @staticmethod
    cdef double func(double x):
        return x-1

ctypedef fused pick_func:
    Plus
    Minus

cdef run_func(double [::1] x, pick_func dummy):
    cdef int i
    with cython.boundscheck(False), cython.wraparound(False):
        for i in range(x.shape[0]):
            x[i] = cython.typeof(dummy).func(x[i])
    return x.base

def run_func_plus(x):
    return run_func[Plus](x,Plus())

def run_func_minus(x):
    return run_func[Minus](x,Minus())

Для сравнения эквивалентный код, использующий указатели функций, равен

cdef double add_one(double x):
    return x+1

cdef double minus_one(double x):
    return x-1

cdef run_func_ptr(double [::1] x, double (*f)(double)):
    cdef int i
    with cython.boundscheck(False), cython.wraparound(False):
        for i in range(x.shape[0]):
            x[i] = f(x[i])
    return x.base

def run_func_ptr_plus(x):
    return run_func_ptr(x,add_one)

def run_func_ptr_minus(x):
    return run_func_ptr(x,minus_one)

При использовании timeit я получаю примерно 2,5-кратное ускорение по сравнению с использованием указателей на функции.Это говорит о том, что указатели на функции не оптимизированы для меня (однако я не пытался изменить настройки компилятора, чтобы попытаться улучшить это)

import numpy as np
import example

# show the two methods give the same answer
print(example.run_func_plus(np.ones((10,))))
print(example.run_func_minus(np.ones((10,))))

print(example.run_func_ptr_plus(np.ones((10,))))
print(example.run_func_ptr_minus(np.ones((10,))))

from timeit import timeit

# timing comparison
print(timeit("""run_func_plus(x)""",
             """from example import run_func_plus
from numpy import zeros
x = zeros((10000,))
""",number=10000))

print(timeit("""run_func_ptr_plus(x)""",
             """from example import run_func_ptr_plus
from numpy import zeros
x = zeros((10000,))
""",number=10000))
0 голосов
/ 03 марта 2019

Это правда, что использование указателя на функцию может иметь некоторые небольшие дополнительные затраты, но большую часть времени снижение производительности происходит из-за того, что компилятор больше не может встроить вызываемую функцию и выполнять оптимизации, которые, где это возможно, после

Я хотел бы продемонстрировать это на следующем примере, который несколько меньше вашего:

int f(int i){
    return i;
}

int sum_with_fun(){
    int sum=0;
    for(int i=0;i<1000;i++){
        sum+=f(i);
    }
    return sum;
}

typedef int(*fun_ptr)(int);
int sum_with_ptr(fun_ptr ptr){
    int sum=0;
    for(int i=0;i<1000;i++){
        sum+=ptr(i);
    }
    return sum;
}

Итак, есть две версии для вычисления sum f(i) for i=0...999: с указателем на функцию и напрямую.

При компиляции с -fno-inline (т. Е. Отключением встраивания для выравнивания земли) они создают почти идентичный ассемблер (здесь на godbolt.org ) - небольшая разница в том, как работает функциязвонил:

callq  4004d0 <_Z1fi>  //direct call
...
callq  *%r12           //via ptr

производительность, это не будет иметь большого значения.

Но когда мы отбрасываем -fno-inline, компилятор может светить для прямой версии, как это и происходит (здесь на godbolt.org )

_Z12sum_with_funv:
        movl    $499500, %eax
        ret

, то естьВесь цикл оценивается во время компиляции, по сравнению с неизменной косвенной версией, которой необходимо выполнить цикл во время выполнения:

_Z12sum_with_ptrPFiiE:
        pushq   %r12
        movq    %rdi, %r12
        pushq   %rbp
        xorl    %ebp, %ebp
        pushq   %rbx
        xorl    %ebx, %ebx
.L5:
        movl    %ebx, %edi
        addl    $1, %ebx
        call    *%r12
        addl    %eax, %ebp
        cmpl    $1000, %ebx
        jne     .L5
        movl    %ebp, %eax
        popq    %rbx
        popq    %rbp
        popq    %r12
        ret

Так где же он вас покинет?Вы могли бы обернуть косвенную функцию известными указателями, и велика вероятность того, что компилятор сможет выполнить вышеописанные оптимизации, см., Например:

... 
int sum_with_f(){
    return sum_with_ptr(&f);
}

приводит к (здесь на godbolt.org ):

_Z10sum_with_fv:
        movl    $499500, %eax
        ret

При вышеуказанном подходе вы зависите от компилятора (но современный компилятор милостив), чтобы выполнить вставку.

Есть такжедругие варианты, в зависимости от того, что вы на самом деле используете:

  1. В C ++ есть шаблоны для устранения такого рода повторяющихся работ без снижения производительности.
  2. В C можно использовать макросы с такими жеэффект.
  3. Numpy использует препроцессор для генерации повторяющегося кода, см., например, этот src-файл , из которого c-файл будет сгенерирован на этапе предварительной обработки.
  4. pandas использует подход, похожий на numpy, для кода на Cython, см., например, hashtable_func_helper.pxi.in-file .
...