根据您发布的代码示例,看起来您想计算垂直最大值,这意味着在您的情况下,“列”是水平的。 在C/C++中,元素的水平序列(即相邻元素在内存中相差一个元素的位置)通常称为行,而垂直序列(其中两个相邻元素的距离为行大小)则称为列。 在下面的回答中,我将使用传统术语,其中行是水平的,列是垂直的。
另外,为了简洁起见,我将关注一种可能的矩阵元素类型-float
。 基本思想对于double
是相同的,主要区别在于每个向量的元素数量和_ps
/_pd
内部选择。 我将在最后提供double
版本。
想法是可以使用_mm_max_ps
/_mm_max_pd
并行计算多个列的垂直最大值。 为了记录找到的最大值的位置,可以将先前的最大值与当前元素进行比较。比较结果是一个掩码,其中元素全都是1,表示更新了最大值。该掩码可用于选择需要更新的位置。
我必须指出,下面的算法假定不重要记录哪个最大元素的位置,如果一列中有多个相等的最大元素。 此外,我假设矩阵不包含NaN值,这会影响比较。 更多信息请参见后文。
void find_max(const int n_cols,
const int n_rows,
const float* val_ptr,
int* opt_pos_ptr,
float* max_ptr){
const __m128i mm_one = _mm_set1_epi32(1);
int tail_size = n_rows & 3;
int n_rows_aligned = n_rows - tail_size;
int row = 0;
for (; row < n_rows_aligned; row += 4)
{
const auto* col_ptr = val_ptr + row;
__m128 mm_max = _mm_loadu_ps(col_ptr);
__m128i mm_max_pos = _mm_setzero_si128();
__m128i mm_pos = mm_one;
col_ptr += n_rows;
for (int col = 1; col < n_cols; ++col)
{
__m128 mm_value = _mm_loadu_ps(col_ptr);
__m128 mm_mask = _mm_cmplt_ps(mm_max, mm_value);
mm_max_pos = _mm_blendv_epi8(mm_max_pos, mm_pos, _mm_castps_si128(mm_mask));
mm_max = _mm_max_ps(mm_value, mm_max);
mm_pos = _mm_add_epi32(mm_pos, mm_one);
col_ptr += n_rows;
}
_mm_storeu_ps(max_ptr + row, mm_max);
_mm_storeu_si128(reinterpret_cast< __m128i* >(opt_pos_ptr + row), mm_max_pos);
}
for (; row < n_rows; ++row)
{
const auto* col_ptr = val_ptr + row;
auto max = *col_ptr;
int max_pos = 0;
col_ptr += n_rows;
for (int col = 1; col < n_cols; ++col)
{
auto value = *col_ptr;
if (value > max)
{
max = value;
max_pos = col;
}
col_ptr += n_rows;
}
max_ptr[row] = max;
opt_pos_ptr[row] = max_pos;
}
}
上述代码由于混合内在函数需要SSE4.1支持。您可以将它们替换为
_mm_and_si128
/
_ps
、
_mm_andnot_si128
/
_ps
和
_mm_or_si128
/
_ps
的组合,这样就可以将要求降低到SSE2。有关特定内在函数的更多详细信息,包括所需的指令集扩展,请参阅
Intel Intrinsics指南。
关于NaN值的注意事项。如果矩阵中可能存在NaN,则
_mm_cmplt_ps
测试将始终返回false。至于
_mm_max_ps
,通常不知道它会返回什么。该内在函数所对应的
maxps
指令,如果其中任一操作数是NaN,则返回第二个(源)操作数,因此通过安排指令的操作数,可以实现任一行为。然而,并没有文档说明
_mm_max_ps
内在函数的哪个参数表示指令的哪个操作数,甚至编译器在不同情况下使用不同的关联也是可能的。有关更多详细信息,请参见
此答案。
为确保对NaNs的正确行为,您可以使用内联汇编以强制正确的
maxps
操作数顺序。不幸的是,对于您正在使用的x86-64目标MSVC而言,这并不是一个选择,因此您可以重用
_mm_cmplt_ps
的结果进行第二个混合,如下所示:
// Compute the maximum
mm_max = _mm_blendv_ps(mm_max, mm_value, mm_mask);
这将抑制结果中的NaN(非数字)。如果您希望保留NaN,可以使用第二个比较来检测NaN:
// Detect NaNs
__m128 mm_nan_mask = _mm_cmpunord_ps(mm_value, mm_value);
// Compute the maximum
mm_max = _mm_blendv_ps(mm_max, mm_value, _mm_or_ps(mm_mask, mm_nan_mask));
如果使用更为粗暴的向量寄存器(
__m256
或
__m512
),并以一个小因子展开外层循环,那么算法性能可以进一步提高,这样就可以保证每次内层循环迭代时至少会加载一整行数据到缓存中。
下面是一个使用
double
的实现例子。需要注意的重点是,由于每个向量仅包含两个元素而向量长度是四个位置,因此我们必须将外层循环展开以便同时处理两个
double
向量,并通过压缩来自与前一最大值的比较所得到的两个掩码以混合 32 位位置。
void find_max(const int n_cols,
const int n_rows,
const double* val_ptr,
int* opt_pos_ptr,
double* max_ptr){
const __m128i mm_one = _mm_set1_epi32(1);
int tail_size = n_rows & 3;
int n_rows_aligned = n_rows - tail_size;
int row = 0;
for (; row < n_rows_aligned; row += 4)
{
const auto* col_ptr = val_ptr + row;
__m128d mm_max1 = _mm_loadu_pd(col_ptr);
__m128d mm_max2 = _mm_loadu_pd(col_ptr + 2);
__m128i mm_max_pos = _mm_setzero_si128();
__m128i mm_pos = mm_one;
col_ptr += n_rows;
for (int col = 1; col < n_cols; ++col)
{
__m128d mm_value1 = _mm_loadu_pd(col_ptr);
__m128d mm_value2 = _mm_loadu_pd(col_ptr + 2);
__m128d mm_mask1 = _mm_cmplt_pd(mm_max1, mm_value1);
__m128d mm_mask2 = _mm_cmplt_pd(mm_max2, mm_value2);
__m128i mm_mask = _mm_packs_epi32(
_mm_castpd_si128(mm_mask1), _mm_castpd_si128(mm_mask2));
mm_max_pos = _mm_blendv_epi8(mm_max_pos, mm_pos, mm_mask);
mm_max1 = _mm_max_pd(mm_value1, mm_max1);
mm_max2 = _mm_max_pd(mm_value2, mm_max2);
mm_pos = _mm_add_epi32(mm_pos, mm_one);
col_ptr += n_rows;
}
_mm_storeu_pd(max_ptr + row, mm_max1);
_mm_storeu_pd(max_ptr + row + 2, mm_max2);
_mm_storeu_si128(reinterpret_cast< __m128i* >(opt_pos_ptr + row), mm_max_pos);
}
if (tail_size >= 2)
{
const auto* col_ptr = val_ptr + row;
__m128d mm_max1 = _mm_loadu_pd(col_ptr);
__m128i mm_max_pos = _mm_setzero_si128();
__m128i mm_pos = mm_one;
col_ptr += n_rows;
for (int col = 1; col < n_cols; ++col)
{
__m128d mm_value1 = _mm_loadu_pd(col_ptr);
__m128d mm_mask1 = _mm_cmplt_pd(mm_max1, mm_value1);
__m128i mm_mask = _mm_packs_epi32(
_mm_castpd_si128(mm_mask1), _mm_castpd_si128(mm_mask1));
mm_max_pos = _mm_blendv_epi8(mm_max_pos, mm_pos, mm_mask);
mm_max1 = _mm_max_pd(mm_value1, mm_max1);
mm_pos = _mm_add_epi32(mm_pos, mm_one);
col_ptr += n_rows;
}
_mm_storeu_pd(max_ptr + row, mm_max1);
_mm_storel_epi64(reinterpret_cast< __m128i* >(opt_pos_ptr + row), mm_max_pos);
row += 2;
}
for (; row < n_rows; ++row)
{
const auto* col_ptr = val_ptr + row;
auto max = *col_ptr;
int max_pos = 0;
col_ptr += n_rows;
for (int col = 1; col < n_cols; ++col)
{
auto value = *col_ptr;
if (value > max)
{
max = value;
max_pos = col;
}
col_ptr += n_rows;
}
max_ptr[row] = max;
opt_pos_ptr[row] = max_pos;
}
}
i
的集合A[i][j]
称为行,将固定j
的集合A[i][j]
称为列,并且C地址是这样的:&A[i][j] =
&A[0][0] + i*NumberOfColumns + j,但是
val_ptr + col * n_rows`与该约定相反。 - Eric Postpischil