为了获得最佳性能,在行交换和行加法中使用一组行指针。使用<stdint.h>
,以及支持的字长最小的快速无符号整数类型 -- 我建议使用uint_fast32_t
,除非您打算在16位或8位处理器上运行。
当所有行交换和行加法完成后,转置数组。虽然这个操作比较“慢”,但以下列操作将非常快,可以抵消转置成本。
请考虑以下内容:
#include <stdint.h>
#include <limits.h>
typedef uint_fast32_t word;
#define WORD_BITS (CHAR_BIT * sizeof (word))
typedef struct {
int rows;
int cols;
int words;
word **row;
} row_matrix;
typedef struct {
int rows;
int cols;
int words;
word **col;
} col_matrix;
虽然您可以使用单个类型来描述这两种矩阵形式,但使用不同的类型使代码和函数更易于维护。最终您可能会有一些重复的代码,但与具有清晰、直观类型相比,这是一个小问题。
在32位系统上,
uint_fast32_t
通常是32位类型。在64位系统上,它通常是64位。
WORD_BITS
宏扩展为
word
中的位数 - 它并不总是32!
编号位的最简单方法是将矩阵中最左边的位指定为位0,并将位存储在每个字中的最低有效位。如果您有
row_matrix * rm
,则行
row
,列
col
处的位为:
!!(rm->row[row][col / WORD_BITS] & ((word)1U << (col % WORD_BITS)))
!!
是非非运算符:如果参数非零,则返回1,否则返回0。因为我们从单词中屏蔽了一个位,所以“位被设置”的值否则将是2的某个幂次方(1、2、4、8、16、32、64等)。
要设置位,请使用:
rm->row[row][col / WORD_BITS] |= (word)1U << (col % WORD_BITS);
为了清除一个比特位,你需要用一个掩码进行按位与运算,该掩码除目标比特位外都为1。可以使用not操作符
~
轻松实现这一点:
rm->row[row][col / WORD_BITS] &= ~((word)1U << (col % WORD_BITS));
col_matrix *cm
的相应操作如下:
!!(cm->col[col][row / WORD_BITS] & ((word)1U << (row % WORD_BITS)))
cm->col[col][row / WORD_BITS] |= (word)1U << (row % WORD_BITS);
cm->col[col][row / WORD_BITS] &= ~((word)1U << (row % WORD_BITS));
尽管除法
/
和模数(或余数)
%
通常比加法、减法甚至乘法慢,但在所有广泛使用的架构上,
WORD_BITS
将是一个二次方编译时常量。我知道的所有编译器都会将上述内容转换为快速位移和二进制AND操作。
要将行
srcrow
添加到行
dstrow
,您只需对所有单词进行二进制异或:
{
const word *const src = rm->row[srcrow];
word *const dst = rm->row[dstrow];
int w = rm->words;
while (w-->0)
dst[w] ^= src[w];
}
同样地,对于列矩阵,
{
const word *const src = cm->col[srccol];
word *const dst = cm->col[dstcol];
int w = cm->words;
while (w-->0)
dst[w] ^= src[w];
}
请注意,如果你合并超过两行,你可以非常高效地完成;这比连续执行加法要快得多。英特尔和AMD的CPU非常擅长预测上述模式,所以你可以使用多个源行/列。此外,目标不一定必须参与结果,尽管如果我猜对了你正在实现的算法,我想你希望它参与其中。
如果你知道目标架构具有SSE2或更好的性能,甚至是AVX,你可以分别使用代码emmintrin.h或immintrin.h头文件来为编译器内置类型和运算符提供支持,允许你一次性异或128位和256位;这有时会给你带来相当大的提升。
由于向量类型需要C标准所谓的“超出对齐”,因此您还需要包括mm_malloc.h,并使用_mm_malloc()和_mm_free()来为单词数据分配行/列向量-显然将单词四舍五入以便您可以将行/列作为适当的整数字类型(适用于SSE*的__m128i,适用于AVX的__m256i)访问。
个人而言,我总是先实现未向量化的版本,然后进行一些“恶意”测试用例进行单元测试,然后再考虑向量化它。这样做的好处是,您可以将未向量化的版本作为初步版本提供给那些将使用它的人,并且您可以比较向量化和未向量化用例之间的测试结果,以查看哪一个有错误。
转置操作非常简单,尽管我建议使用三重循环:最内部循环在单个字中循环位。此外,你可能想检查哪个顺序——行或列主要——对于外循环效果最好;取决于矩阵大小,你可能会看到很大的差异。(这是由于缓存行为:你希望CPU能够预测访问模式,而不必重新加载相同的缓存行。在最好的情况下,在至多几年前的AMD和Intel x86-64处理器上,如果两个矩阵都适合缓存,你可以获得接近缓存速度的速度。)
以上所有内容都可以实现在一个头文件中——甚至包括向量化版本,如果目标架构支持SSE2/AVX——因此应该不难实现。
有问题吗?
1
和0
的矩阵的最佳方法,同时给出后续需要应用的操作。 - Celerio