我正在对串行和Cilk数组表示法版本的矩阵乘法性能进行基准测试。 Cilk的实现几乎是串口的两倍,我不明白为什么。
这是针对单核执行的
这是Cilk乘法的核心。由于排名限制,我必须在设置目标矩阵元素值之前将每个乘法存储在数组中,然后__sec_reduce_add
此数组。
int* sum = new int[VEC_SIZE];
for (int r = 0; (r < rowsThis); r++) {
for (int c = 0; (c < colsOther); c++) {
sum[0:VEC_SIZE] = myData[r][0:VEC_SIZE] * otherData[0:VEC_SIZE][c];
product.data[r][c] = __sec_reduce_add(sum[0:VEC_SIZE]);
}
}
我理解缓存问题,并且没有看到任何理由让Cilk版本的缓存命中数比串行更少,因为它们都访问一个有希望在缓存中的列数组以及行元素的一系列未命中。
我应该使用的是我忽视的明显依赖还是Cilk的语法元素?我应该以不同的方式使用Cilk来实现使用SIMD操作的非块矩阵乘法的最大性能吗?
我对Cilk很新,所以欢迎任何帮助/建议。
编辑:
这是串行实现:
for (int row = 0; (row < rowsThis); row++) {
for (int col = 0; (col < colsOther); col++) {
int sum = 0;
for (int i = 0; (i < VEC_SIZE); i++) {
sum += (matrix1[row][i] * matrix2[i][col]);
}
matrix3[row][col] = sum;
}
}
适当地分配(de)内存,并且两个实现的乘法结果都是正确的。在编译时不知道矩阵大小,并且在整个基准测试中使用了各种各样的矩阵。
编译器:icpc(ICC)15.0.0 20140723
编译标志:icpc -g Wall -O2 -std = c ++ 11
忽略使用已分配的内存,从向量到香草数组的转换等。我劈开了另一个程序来预测它运行它,假设它比事实证明的更简单。我无法让编译器在2D向量的两个维度上接受cilk表示法,所以我决定使用传统数组来进行基准测试。
以下是所有适当的文件:
MatrixTest.cpp
#include <string>
#include <fstream>
#include <stdlib.h>
#include "Matrix.h"
#define MATRIX_SIZE 2000
#define REPETITIONS 1
int main(int argc, char** argv) {
auto init = [](int row, int col) { return row + col; };
const Matrix matrix1(MATRIX_SIZE, MATRIX_SIZE, init);
const Matrix matrix2(MATRIX_SIZE, MATRIX_SIZE, init);
for (size_t i = 0; (i < REPETITIONS); i++) {
const Matrix matrix3 = matrix1 * matrix2;
std::cout << "Diag sum: " << matrix3.sumDiag() << std::endl;
}
return 0;
}
Matrix.h
#ifndef MATRIX_H
#define MATRIX_H
#include <iostream>
#include <functional>
#include <vector>
using TwoDVec = std::vector<std::vector<int>>;
class Matrix {
public:
Matrix();
~Matrix();
Matrix(int rows, int cols);
Matrix(int rows, int cols, std::function<Val(int, int)> init);
Matrix(const Matrix& src);
Matrix(Matrix&& src);
Matrix operator*(const Matrix& src) const throw(std::exception);
Matrix& operator=(const Matrix& src);
int sumDiag() const;
protected:
int** getRawData() const;
private:
TwoDVec data;
};
#endif
Matrix.cpp
#include <iostream>
#include <algorithm>
#include <stdexcept>
#include "Matrix.h"
#if defined(CILK)
Matrix
Matrix::operator*(const Matrix& other) const throw(std::exception) {
int rowsOther = other.data.size();
int colsOther = rowsOther > 0 ? other.data[0].size() : 0;
int rowsThis = data.size();
int colsThis = rowsThis > 0 ? data[0].size() : 0;
if (colsThis != rowsOther) {
throw std::runtime_error("Invalid matrices for multiplication.");
}
int** thisRaw = this->getRawData(); // held until d'tor
int** otherRaw = other.getRawData();
Matrix product(rowsThis, colsOther);
const int VEC_SIZE = colsThis;
for (int r = 0; (r < rowsThis); r++) {
for (int c = 0; (c < colsOther); c++) {
product.data[r][c] = __sec_reduce_add(thisRaw[r][0:VEC_SIZE]
* otherRaw[0:VEC_SIZE][c]);
}
}
delete[] thisRaw;
delete[] otherRaw;
return product;
}
#elif defined(SERIAL)
Matrix
Matrix::operator*(const Matrix& other) const throw(std::exception) {
int rowsOther = other.data.size();
int colsOther = rowsOther > 0 ? other.data[0].size() : 0;
int rowsThis = data.size();
int colsThis = rowsThis > 0 ? data[0].size() : 0;
if (colsThis != rowsOther) {
throw std::runtime_error("Invalid matrices for multiplication.");
}
int** thisRaw = this->getRawData(); // held until d'tor
int** otherRaw = other.getRawData();
Matrix product(rowsThis, colsOther);
const int VEC_SIZE = colsThis;
for (int r = 0; (r < rowsThis); r++) {
for (int c = 0; (c < colsOther); c++) {
int sum = 0;
for (int i = 0; (i < VEC_SIZE); i++) {
sum += (thisRaw[r][i] * otherRaw[i][c]);
}
product.data[r][c] = sum;
}
}
delete[] thisRaw;
delete[] otherRaw;
return product;
}
#endif
// Default c'tor
Matrix::Matrix()
: Matrix(1,1) { }
Matrix::~Matrix() { }
// Rows/Cols c'tor
Matrix::Matrix(int rows, int cols)
: data(TwoDVec(rows, std::vector<int>(cols, 0))) { }
// Init func c'tor
Matrix::Matrix(int rows, int cols, std::function<int(int, int)> init)
: Matrix(rows, cols) {
for (int r = 0; (r < rows); r++) {
for (int c = 0; (c < cols); c++) {
data[r][c] = init(r,c);
}
}
}
// Copy c'tor
Matrix::Matrix(const Matrix& other) {
int rows = other.data.size();
int cols = rows > 0 ? other.data[0].size() : 0;
this->data.resize(rows, std::vector<int>(cols, 0));
for(int r = 0; (r < rows); r++) {
this->data[r] = other.data[r];
}
}
// Move c'tor
Matrix::Matrix(Matrix&& other) {
if (this == &other) return;
this->data.clear();
int rows = other.data.size();
for (int r = 0; (r < rows); r++) {
this->data[r] = std::move(other.data[r]);
}
}
Matrix&
Matrix::operator=(const Matrix& other) {
int rows = other.data.size();
this->data.resize(rows, std::vector<int>(0));
for (int r = 0; (r < rows); r++) {
this->data[r] = other.data[r];
}
return *this;
}
int
Matrix::sumDiag() const {
int rows = data.size();
int cols = rows > 0 ? data[0].size() : 0;
int dim = (rows < cols ? rows : cols);
int sum = 0;
for (int i = 0; (i < dim); i++) {
sum += data[i][i];
}
return sum;
}
int**
Matrix::getRawData() const {
int** rawData = new int*[data.size()];
for (int i = 0; (i < data.size()); i++) {
rawData[i] = const_cast<int*>(data[i].data());
}
return rawData;
}
答案 0 :(得分:1)
[2015-3-30更新以匹配长代码示例。]
icc可能会自动对您的累积循环进行矢量化,因此Cilk Plus正在与矢量化代码竞争。这里有两个可能的性能问题:
临时数组总和会增加加载和存储的数量。串行代码仅执行两个(SIMD)加载,并且每个(SIMD)乘法几乎没有存储。对于临时数组,每次乘法有三个加载和一个存储。
matrix2具有非单位步幅访问模式(在串行代码中也是如此)。通过单位步幅访问,典型的当前硬件工作得更好。
要修复问题(1),请删除临时数组,就像稍后在较长的样本中所做的那样。 E.g:
for (int r = 0; (r < rowsThis); r++) {
for (int c = 0; (c < colsOther); c++) {
product.data[r][c] = __sec_reduce_add(thisRaw[r][0:VEC_SIZE] * otherRaw[0:VEC_SIZE][c]);
}
}
__sec_reduce_add
的结果等级为零,因此可以分配给标量。
更好的是,解决步伐问题。除非结果矩阵非常宽,否则一个好的方法是按行累积结果,如下所示:
for (int r = 0; (r < rowsThis); r++) {
product.data[r].data()[0:colsOther] = 0;
for (int k = 0; (k < VEC_SIZE); k++) {
product.data[r].data()[0:colsOther] +=thisRaw[r][k] * otherRaw[k][0:colsOther];
}
}
请注意此处使用data()
。数组表示法当前不允许将部分表示法与来自[]
的重载std::vector
一起使用。所以我使用data()
来获取指向底层数据的指针,我可以使用数组表示法。
现在数组部分都有单位步长,因此编译器可以有效地使用矢量硬件。上面的方案通常是我最喜欢的构建无阻塞矩阵乘法的方法。使用icpc -g -Wall -O2 -xHost -std=c++11 -DCILK
进行编译并在i7-4770处理器上运行时,我看到MatrixTest程序时间从大约52秒降至1.75秒,性能提升了29倍。
通过消除归零(在构造product
时已经完成),可以简化和加快代码,并消除临时指针数组的构造。以下是修订后的运算符*的完整定义:
Matrix
Matrix::operator*(const Matrix& other) const throw(std::exception) {
int rowsOther = other.data.size();
int colsOther = rowsOther > 0 ? other.data[0].size() : 0;
int rowsThis = data.size();
int colsThis = rowsThis > 0 ? data[0].size() : 0;
if (colsThis != rowsOther) {
throw std::runtime_error("Invalid matrices for multiplication.");
}
Matrix product(rowsThis, colsOther);
for (int r = 0; (r < rowsThis); r++) {
product.data[r].data()[0:colsOther] = 0;
for (int k = 0; (k < colsThis); k++) {
product.data[r].data()[0:colsOther] += (*this).data[r][k] * other.data[k].data()[0:colsOther];
}
}
return product;
}
如果Matrix
有计算data[i].data()
的方法,那么长行会更短。