C - Матрицы как передать по значению?

Я разрабатываю функции обработки матрицы для C-проекта. Я рассматриваю либо передачу матриц по значению или по ссылке. Я создал бенчмарк, передающий матрицы по значению и по ссылке, и оба, кажется, выполняют то же самое с флагами оптимизации -O0 и -O2 в gcc. Учитывая, что мой тест может давать неправильные результаты, я хотел бы знать, каков наиболее эффективный способ передачи матриц в вызовы функций и из них с использованием только C.

#include <stdio.h>
#include <time.h>

// Compiled on OSX 10.6.8 using: cc -o matrix matrix.c -std=c99 -O2

typedef struct {
    float m0;
    float m1;
    float m2;
    float m3;
    float m4;
    float m5;
    float m6;
    float m7;
    float m8;
    float m9;
    float m10;
    float m11;
    float m12;
    float m13;
    float m14;
    float m15;
} Matrix;

// ================================================
//                 Pass By Value
// ------------------------------------------------

Matrix PassByValue (Matrix a, Matrix b) {
    Matrix matrix;

    matrix.m0  = a.m0 * b.m0  + a.m4 * b.m1  + a.m8  * b.m2  + a.m12 * b.m3;
    matrix.m1  = a.m1 * b.m0  + a.m5 * b.m1  + a.m9  * b.m2  + a.m13 * b.m3;
    matrix.m2  = a.m2 * b.m0  + a.m6 * b.m1  + a.m10 * b.m2  + a.m14 * b.m3;
    matrix.m3  = a.m3 * b.m0  + a.m7 * b.m1  + a.m11 * b.m2  + a.m15 * b.m3;

    matrix.m4  = a.m0 * b.m4  + a.m4 * b.m5  + a.m8  * b.m6  + a.m12 * b.m7;
    matrix.m5  = a.m1 * b.m4  + a.m5 * b.m5  + a.m9  * b.m6  + a.m13 * b.m7;
    matrix.m6  = a.m2 * b.m4  + a.m6 * b.m5  + a.m10 * b.m6  + a.m14 * b.m7;
    matrix.m7  = a.m3 * b.m4  + a.m7 * b.m5  + a.m11 * b.m6  + a.m15 * b.m7;

    matrix.m8  = a.m0 * b.m8  + a.m4 * b.m9  + a.m8  * b.m10 + a.m12 * b.m11;
    matrix.m9  = a.m1 * b.m8  + a.m5 * b.m9  + a.m9  * b.m10 + a.m13 * b.m11;
    matrix.m10 = a.m2 * b.m8  + a.m6 * b.m9  + a.m10 * b.m10 + a.m14 * b.m11;
    matrix.m11 = a.m3 * b.m8  + a.m7 * b.m9  + a.m11 * b.m10 + a.m15 * b.m11;

    matrix.m12 = a.m0 * b.m12 + a.m4 * b.m13 + a.m8  * b.m14 + a.m12 * b.m15;
    matrix.m13 = a.m1 * b.m12 + a.m5 * b.m13 + a.m9  * b.m14 + a.m13 * b.m15;
    matrix.m14 = a.m2 * b.m12 + a.m6 * b.m13 + a.m10 * b.m14 + a.m14 * b.m15;
    matrix.m15 = a.m3 * b.m12 + a.m7 * b.m13 + a.m11 * b.m14 + a.m15 * b.m15;

    return matrix;
}


// ================================================
//               Pass By Reference
// ------------------------------------------------

void PassByReference (Matrix* matrix, Matrix* a, Matrix* b) {
    if (!matrix) return;
    if (!a) return;
    if (!b) return;

    matrix->m0  = a->m0 * b->m0  + a->m4 * b->m1  + a->m8  * b->m2  + a->m12 * b->m3;
    matrix->m1  = a->m1 * b->m0  + a->m5 * b->m1  + a->m9  * b->m2  + a->m13 * b->m3;
    matrix->m2  = a->m2 * b->m0  + a->m6 * b->m1  + a->m10 * b->m2  + a->m14 * b->m3;
    matrix->m3  = a->m3 * b->m0  + a->m7 * b->m1  + a->m11 * b->m2  + a->m15 * b->m3;

    matrix->m4  = a->m0 * b->m4  + a->m4 * b->m5  + a->m8  * b->m6  + a->m12 * b->m7;
    matrix->m5  = a->m1 * b->m4  + a->m5 * b->m5  + a->m9  * b->m6  + a->m13 * b->m7;
    matrix->m6  = a->m2 * b->m4  + a->m6 * b->m5  + a->m10 * b->m6  + a->m14 * b->m7;
    matrix->m7  = a->m3 * b->m4  + a->m7 * b->m5  + a->m11 * b->m6  + a->m15 * b->m7;

    matrix->m8  = a->m0 * b->m8  + a->m4 * b->m9  + a->m8  * b->m10 + a->m12 * b->m11;
    matrix->m9  = a->m1 * b->m8  + a->m5 * b->m9  + a->m9  * b->m10 + a->m13 * b->m11;
    matrix->m10 = a->m2 * b->m8  + a->m6 * b->m9  + a->m10 * b->m10 + a->m14 * b->m11;
    matrix->m11 = a->m3 * b->m8  + a->m7 * b->m9  + a->m11 * b->m10 + a->m15 * b->m11;

    matrix->m12 = a->m0 * b->m12 + a->m4 * b->m13 + a->m8  * b->m14 + a->m12 * b->m15;
    matrix->m13 = a->m1 * b->m12 + a->m5 * b->m13 + a->m9  * b->m14 + a->m13 * b->m15;
    matrix->m14 = a->m2 * b->m12 + a->m6 * b->m13 + a->m10 * b->m14 + a->m14 * b->m15;
    matrix->m15 = a->m3 * b->m12 + a->m7 * b->m13 + a->m11 * b->m14 + a->m15 * b->m15;
}

// ================================================
//                  Benchmark
// ------------------------------------------------

#define LOOPS 100000

int main () {
    Matrix result;
    Matrix a;
    Matrix b;
    clock_t begin;
    clock_t end;
    int index;

    // ------------------------------------------
    //          Pass By Reference
    // ------------------------------------------
    begin = clock();
    for (index = 0; index < LOOPS; index++) {

        PassByReference(&result,&a,&b);
        a.m0 += index;
        b.m0 += index;

    }
    end = clock();
    printf("Pass By Ref: %f\n",(double)(end - begin) / CLOCKS_PER_SEC);

    // ------------------------------------------
    //            Pass By Value
    // ------------------------------------------
    begin = clock();
    for (index = 0; index < LOOPS; index++) {

        result = PassByValue(a,b);
        a.m0 += index;
        b.m0 += index;

    }
    end = clock();
    printf("Pass By Val: %f\n",(double)(end - begin) / CLOCKS_PER_SEC);


    // The following line along with the above
    // additions in the loops hopefully prevent
    // the matrices from being optimized into
    // nothing.
    printf("%0.1f\n",result.m0);

    return 0;
}

Результаты:

Pass By Ref: 0.489226
Pass By Val: 0.488882

4 ответа

Решение

У вас есть 2 конкурирующих интересов здесь:

  1. передавая структуру по значению, это типизируется как класс хранения данных и помещается в стек в соответствии с соглашением о вызовах x86, это немного медленнее, чем вызов по ref, который застревает в регистре.

  2. это почти точно сбалансировано кучей разыменований указателей...

отделить и профилировать каждую часть отдельно

если вы пытаетесь сделать этот вид кода быстрее, вы сможете писать более быстрые реализации в каком-то виде кода SIMD, AltiVec, SSE или OpenCL в зависимости от

32 значения с плавающей запятой не будут вписываться в регистры в любом случае. Компилятор будет вынужден перенести данные из памяти в стек, который является просто другой частью памяти. В зависимости от количества обращений к данным копирование данных может быть даже медленнее, чем разыменование указателей.

Я бы предложил использовать передачу по ссылке с модификатором const для любых нескалярных данных. Задача компилятора - оптимизировать ваш код для конкретных платформ.

Из Эффективного C++:

Предпочитайте передачу по ссылке на константу перед передачей по значению, обычно она более эффективна и позволяет избежать проблемы срезов. Правило не применяется к встроенным типам и итераторам STL и типам объектов функций. Для них обычно подходит передача по значению.

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

Однако, как сказал автор Effective C++,

некоторые компиляторы отказываются помещать объекты, состоящие только из двойников, в регистр, хотя они с радостью регулярно размещают там голые двойники. Когда такое происходит, вам лучше передать такие объекты по ссылке, потому что компиляторы, безусловно, будут помещать указатели в регистры ".Unsubscribe-lgm-thur

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

Технически, у нас есть только "передача по значению" в C. Вы должны передать матричные указатели (по значению) в функцию. Это уменьшит объем данных, "скопированных" в функцию, и, следовательно, станет более эффективным.

Другие вопросы по тегам