Каковы штрафы универсальных функций по сравнению с массивами указателей функций в C?


Я пишу матричную библиотеку (часть SciRuby) с несколькими типами хранения ("stypes") и несколькими типами данных ("dtypes"). Например, Матрица stype в настоящее время может быть плотной, yale (он же "csr") или list-of-lists; и ее dtype может быть int8, int16, int32, int64, float32, float64, complex64, и т.д.

Очень легко написать шаблонный процессор в Ruby или sed, который берет базовую функцию (например, разреженное умножение матрицы) и создает пользовательскую версию для каждого возможного dtype. Я можно даже написать такой шаблон для обработки двух разных типов dtypes, скажем, если мы хотим умножить int32 на float64.

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

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

MultFuncs[lhs->stype][lhs->dtype][rhs->dtype](lhs->shape[0], rhs->shape[1], lhs->data, rhs->data, result->data);
// This might point to some function like this:
// i32_f64_dense_mult(size_t, size_t, int32_t*, float64*, float64*);
Конечно, экстремальной альтернативой массивам указателей функций, которые было бы невероятно сложно кодировать и поддерживать, является иерархическая switch или if/else заявления:
switch(lhs->stype) {
case STYPE_SPARSE:
  switch(lhs->dtype) {
  case DTYPE_INT32:
    switch(rhs->dtype) {
    case DTYPE_FLOAT64:
      i32_f64_mult(lhs->shape[0], rhs->shape[1], lhs->ija, rhs->ija, lhs->a, rhs->a, result->data);
      break;
    // ... and so on ...

Также кажется, что это будет O (sd2), где s =число stypes, d =число dtypes для каждой операции, тогда как массив указателей функций будет O (r ), где r =число измерений в массив.

Но есть и третий вариант.

Третий вариант заключается в использовании массивов указателей функций для общих операций (например, копирование из одного неизвестного типа в другой):
SetFuncs[lhs->dtype][rhs->dtype](5, // copy five consecutive items
 &to, // destination
 dtype_sizeof[lhs->dtype], // dtype_sizeof is a const size_t array giving sizeof(int8_t), sizeof(int16_t), etc.
 &from, // source
 dtype_sizeof[rhs->dtype]);

И затем вызвать это из общей разреженной функции умножения матрицы, которая может быть объявлена следующим образом:

void generic_sparse_multiply(size_t* ija, size_t* ijb, void* a, void* b, int dtype_a, int dtype_b);

И это будет использовать SetFuncs[dtype_a][dtype_b] для ссылки на правильную функцию присваивания, например. Недостатком, таким образом, является то, что вам, возможно, придется реализовать целую кучу эти ... IncrementFuncs, DecrementFuncs, MultFuncs, AddFuncs, SubFuncs и т. д. -- потому что ты никогда не знаешь, каких типов ожидать.

Итак, наконец, мои вопросы:

  1. какова стоимость, если таковая имеется, наличия огромных многомерных массивов констант указателей функций? Большая библиотека или исполняемый файл? Медленное время загрузки? и т.д.
  2. использует ли дженерики как IncrementFuncs, SetFuncs, и т.д. (которые все, вероятно, зависят от memcpy или typecasts) представляют собой барьеры для компиляции оптимизация?
  3. Если использовать операторы switch, как описано выше, будут ли они оптимизированы современными компиляторами? Или они будут оцениваться каждый раз?
Я понимаю, что это невероятно сложный набор вопросов. Если вы можете просто сослаться на хороший ресурс и предпочитаете не отвечать прямо, это прекрасно. Я широко использовал Google, прежде чем опубликовать это, но не был уверен, какие поисковые запросы использовать.
1 2

1 ответ:

Прежде всего, попробуйте уменьшить сложность функции (функций). Вы должны иметь возможность иметь объявление, как

Result_t function (Param_t*);

Где Param_t-это структура, содержащая все те вещи, которые вы передаете. Чтобы использовать универсальные типы, включите в структуру перечисление, указывающее, какой тип используется, и void* для этого типа.


1.Какова стоимость, если таковая имеется, наличия огромных многомерных массивов констант указателей функций? Большая библиотека или исполняемый файл? Медленный время загрузки? и т.д.

Определенно больший исполняемый файл. Время загрузки зависит от того, для какой системы предназначен код. Если это для системы, основанной на оперативной памяти (ПК и т. д.), то время загрузки может увеличиться, но это не должно иметь существенного влияния на производительность. Хотя, конечно, это зависит от того, насколько велик "огромный":)

2.Использует ли дженерики, такие как IncrementFuncs, SetFuncs и т. д. (которые все, вероятно, зависят от memcpy или typecasts) представляют собой барьеры для оптимизация времени компиляции?

Вероятно нет, просто компилятор может оптимизировать очень многое. При работе с универсальными типами данных в C, это часто сводится к memcpy () в конце концов, который, как мы надеемся, будет реализован так же быстро, как копирование.

3.Если использовать операторы switch, как описано выше, будут ли они оптимизированы современными компиляторами? Или они будут оценены каждый раз?

Как ни странно, компилятор, вероятно, оптимизирует его в нечто вроде массива указатель функции. Однако компилятор, скорее всего, не сможет предсказать природу данных, особенно если они будут установлены во время выполнения.