Программирование на Intel Cilk Plus

Элементные функции. Функции прикладного программного интерфейса

Показывать лекцию целиком

Презентацию к лекции Вы можете скачать здесь.

Эффективная векторизация – как её добиться?

Векторизация в рамках C/C++. Недостаток – не всегда удобная реализация.

Использование расширения процессорных инструкций SSE (Streaming SIMD Extension). Недостаток – фиксированная длина вектора.

Использование элементных операций/функций Intel® CilkTM Plus.

Элементные функции

Элементные функции формируют результат вычисления скалярной функции для каждого элемента массива (вызов скалярной функции с векторным аргументом формирует массив значений, конформный аргументу):

__declspec(vector) <сигнатура функции>

Отображение заменяет цикл последовательной программы.

Примеры:

a[:] = sin(b[:]);
a[:] = pow(b[:], c); // b[:]**c
a[:] = pow(c, b[:]); // c**b[:]
f(b[:])

При компиляции кода с вызовом элементных функций компилятор генерирует обращения к векторизованным функциям.

Компилятор может генерировать многопоточный код.

Если функция определена как elemental, компилятор генерирует векторизованный код для этой функции.

Исключены побочные эффекты.

Функции отображаются параллельно!

Ограничения

  • Допускается использование только следующих типов:
  • signed/unsigned 8/16/32/64 битовые целые;
  • 32 или 64 битовые с плавающей точкой;
  • 64 или 128 битовые комплексные;
  • указатель или ссылка C++.
  • Не допускается использование ключевых слов for, while, do, goto.
  • Не допускается использование операторов выбора.
  • Не допускается использование ассемблерных вставок.
  • В функциях не допускается многопоточность, реализованная с помощью , OpenMP, cilk_spawn/cilk_for.
  • Не допускаются виртуальные функции и указатели на функции.
  • В функциях не допускается использование выражений с индексной нотацией.
  • и другие.

    Пример

    float saxpy(float a, float *x, float *y);
    void foo(float *x, float *y, float a, int len) {
       for(int i = 0; i < len; i++)
            saxpy(a, x[i], y[i]);
    }
    
    
    void saxpy(float a, float *x, float *y) {
       *y += a * (*x); 
    }
    
    __declspec(vector(scalar(a),linear(x),linear(y)))
    void saxpy(float a, float *x, float *y);
    void foo(float *restrict x, float *restrict y, float a, int len) {
          saxpy(a, x[0:len], y[0:len]);
    }
    

    Компилятор сгенерирует вот такой код:

    void saxpy_4(float a, float x[4],  float y[4]) 
    {
        y[:] += a * x[:];
    }
    for(i = 0; i < len-3; i += 4) {   
        saxpy_4(a, x[i], y[i]);
    }
    for(; i < len; i++) {   
        saxpy(a, x[i], y[i]);
    }
    
    

    Примеры применения (по предметным областям):

  • обработка изображений (гамма-коррекция и т.д.);
  • преобразование между цветовыми пространствами;
  • моделирование методом Монте-Карло.
  • Дополнительные примеры использования отображения функций

    Параллелизм потоков

    cilk_for( int i=0; i<n; ++i )
        a[i] = f(b[i]);

    Векторный параллелизм

    a[0:n] = f(b[i:n]);
    
    #pragma simd
    for( int i=0; i<n; ++i )
        a[i] = f(b[i]);
    

    Пример использования отображения функций в Intel® Threading Building Blocks

    parallel_for( 0, n, []( int i ) {
        a[i] = f(b[i]);
    });
    
    parallel_for( 
        blocked_range<int>(0,n), 
        [](blocked_range<int> r ) {
            for( int i=r.begin(); i!=r.end(); ++i ) 
                a[i] = f(b[i]);
        });
    

    Операции приведения (редукции)

    Операция редукции применяется к сечению массива. Её результат – скалярное значение.

    Примеры:

    __sec_reduce(f, a[:])
    __sec_reduce_add(a[:])
    

    Базовые типы C поддерживаются следующими операциями редукции:

    add
    mul
    max
    max_ind
    min
    min_ind
    all_zero
    all_non_zero
    any_nonzero
    

    Есть возможность определить пользовательскую операцию приведения:

    type fn(type in1, type in2);
    out = __sec_reduce(fn, identity_value, in[x:y:z]);
    

    Пример использования пользовательской операции редукции

    #include <iostream>
    
    unsigned int bitwise_and(unsigned int x, unsigned int y) {
       return (x  y);
    }
    
    int main() {
       unsigned int a[4] = {5,7,13,15};
       unsigned int b = 0;
    
       std::cout << "Display a:\n" << a[:] << " ";
       std::cout << std::endl << std::endl;
    
       b = __sec_reduce(bitwise_and, 0xffffffff, a[:]);
    
       std::cout << "b:\n" << b << std::endl;
       return(0);
    }
    

    Примеры применения (по предметным областям):

  • матричные операции;
  • обработка изображений;
  • вычисление средних при моделировании методом Монте- Карло.
  • Дополнительные примеры использования операции редукции

    float sum = __sec_reduce_add(a[i:n]);
    
    
    #pragma simd reduction(+:sum)
    float sum=0;
    for( int i=0; i<n; ++i )
        sum += a[i];
    
    cilk::reducer_opadd<float> sum = 0;
    cilk_for( int i=0; i<n; ++i )
        sum += a[i];
    ... = sum.get_value();
    

    Пример использования операции редукции в Intel® Threading Building Blocks

    enumerable_thread_specific<float> sum;
    parallel_for( 0, n, []( int i ) {
        sum.local() += a[i];
    });
    ... = sum.combine(std::plus<float>());
    
    sum = parallel_reduce( 
        blocked_range<int>(0,n), 
        0.f,
        [](blocked_range<int> r, float s) -> float 
        {
            for( int i=r.begin(); i!=r.end(); ++i )
                s +=  a[i];
            return s;
        },
        std::plus<float>()
    

    Функции прикладного программного интерфейса

    Функции ППИ позволяют управлять поведением программы.

    Функции прикладного программного интерфейса (ППИ) используются с заголовочным файлом cilk/cilk_api.h

    int __cilkrts_set_param(const char* name, const char* value);

    Эта функция используется для управления некоторыми параметрами системы исполнения Cilk.

    Первые 2 параметра строковые.

    nworkers – значение определяет количество исполнителей. Если данная функция не используется, количество исполнителей задаётся с помощью переменной окружения CILK_NWORKERS или, по умолчанию, оно равно количеству ядер.

    Данная функция действует только до первого использования cilk_spawn или cilk_for.

    int __cilkrts_get_nworkers(void);

    Эта функция возвращает количество потоков-исполнителей и фиксирует его так, что оно не может быть изменено вызовом функции __cilkrts_set_param.

    пользуется для управления некоторыми параметрами системы исполнения Cilk.

    Идентификаторы исполнителей не обязательно прнимают непрерывный (последовательный) ряд значений.

    int __cilkrts_get_worker_number(void);

    Эта функция возвращает целое значение, показывающее исполнителя, который выполняет функцию.

    int __cilkrts_get_total_workers(void);

    Эта функция возвращает суммарное количество потоков-исполнителей, включая неактивные.

    Несколько советов по повышению производительности

    Оптимизируйте в первую очередь последовательный код.

    Выбор зернистости:

  • избегайте порождения маленьких задач;
  • оптимизируйте зернистость параллельных циклов;
  • мелкозернистая декомпозиция => большие накладные расходы;
  • крупнозернистая декомпозиция => низкий параллелизм, неэффективное использование возможностей вычислительной системы.
  • Оптимизируйте кэш-эффективность.

    Пример false-sharing:

    volatile int x[32];
    void f(volatile int *p)
    {
    for (int i = 0; i < 100000000; i++)
    {
    ++p[0];
    ++p[16];
    }
    }
    int main()
    {
    cilk_spawn f(x[0]);
    cilk_spawn f(x[1]);
    cilk_spawn f(x[2]);
    cilk_spawn f(x[3]);
    cilk_sync;
    return 0;
    }
    Вернуться к учебному плану