Я пишу матричную библиотеку (часть SciRuby) с несколькими типами хранения («стили») и несколькими типами данных («dtypes»). Например, матрица stype
в настоящее время может быть плотной, йельской (AKA 'csr') или списком списков; и dtype
может быть int8
, int16
, int32
, int64
, float32
, float64
, complex64
и т. д.
Очень просто написать шаблонный процессор на Ruby или sed, который принимает базовую функцию (например, умножение разреженных матриц) и создает собственную версию для каждого возможного dtype
. Я мог бы даже написать такой шаблон для обработки двух разных типов, скажем, если бы мы хотели умножить int32
на float64
.
То же самое можно сделать в некоторых случаях для разных стилей. В конечном счете, однако, вы можете получить очень большой набор функций, многие из которых даже не используются в процессе использования большинством людей.
Также легко использовать массивы указателей на функции для обеспечения доступа к этим функциям - и представить даже массив трехмерных указателей на функции не так уж сложно:
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 ( sd 2 ), где s = количество стилей, 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 и т. Д. - потому что вы никогда не будете знать, какие типы ожидать.
Итак, наконец, мои вопросы:
- Какова стоимость, если таковая имеется, наличия огромных многомерных константных массивов указателей функций? Большая библиотека или исполняемый файл? Медленное время загрузки? и т.д.
- Представляет ли использование обобщений, таких как
IncrementFuncs
, SetFuncs
и т. Д. (Которые, вероятно, зависят от memcpy
или типов), барьеры для оптимизации во время компиляции?
- Если использовать операторы switch, как описано выше, будут ли они оптимизированы современными компиляторами? Или они будут оцениваться каждый раз?
Я понимаю, что это невероятно сложный набор вопросов.
Если вы можете просто отослать меня к хорошему ресурсу и предпочесть не отвечать напрямую, это прекрасно. Я много использовал Google, прежде чем опубликовать это, но не был уверен, какие поисковые термины использовать.