The Task Parallel Library Sampler – Part 8: Adding a New Sample, Matrices Multiplication

Part One: Starting with MVVM
Part Two: The MVVM solution structure and basic framework
Part Three: Base Classes
Part 4: Sampler View, View Model and Model
Part 5: Running and working with the TPL samples
Part 6: Parallel.For Sample
Part 7: Using Parallel.For effectively

We’re going to add in two new samples to continue showing the benefits of utilizing the TPL by doing some matrix multiplication. As mentioned before I added this sample for the initial code I created for the mentoring session on the TPL because this sample was directly applicable to some of the work we do. These derive from a sample provided by Microsoft on How to: Write a Simple Parallel.For Loop. Here the samples are pretty straight-forward. What’s more important in this post is what I had to do to add these new samples to the MVVM solution.

MatricesMultiplicationSample:

public class MatricesMultiplicationSample : Sample
{
	// Set up matrices. Use small values to better view 
	// result matrix. Increase the counts to see greater 
	// speedup in the parallel loop vs. the sequential loop.
	protected static readonly int colCount = 180;
	protected static readonly int rowCount = 2000;
	protected static readonly int colCount2 = 270;

	//protected statics so these can be used in the other sample
	static readonly Lazy<double[,]> _lazyMatrix1 = new Lazy<double[,]>(() => { return InitializeRandomMatrix(rowCount, colCount); });
	protected static double[,] Matrix1
	{
		get { return _lazyMatrix1.Value; }
	}

	static readonly Lazy<double[,]> _lazyMatrix2 = new Lazy<double[,]>(() => { return InitializeRandomMatrix(colCount, colCount2); });
	protected static double[,] Matrix2
	{
		get { return _lazyMatrix2.Value; }
	}

	static Random ran = new Random();
	static double[,] InitializeRandomMatrix(int rows, int cols)
	{
		double[,] matrix = new double[rows, cols];

		for (int i = 0; i < rows; i++)
		{
			for (int j = 0; j < cols; j++)
			{
				matrix[i, j] = ran.Next(100);
			}
		}
		return matrix;
	}

	public override string SampleName
	{
		get { return "Matrices Multiplication"; }
	}

	public override bool ImageRequired
	{
		get { return false; }
	}

	public override void Run(System.Drawing.Bitmap bmp = null, Action<string> updateLog = null)
	{
		double[,] matA = Matrix1;
		double[,] matB = Matrix2;
		double[,] result = new double[rowCount, colCount2];

		Stopwatch s = new Stopwatch();
		s.Start();

		int matACols = matA.GetLength(1);
		int matBCols = matB.GetLength(1);
		int matARows = matA.GetLength(0);

		for (int i = 0; i < matARows; i++)
		{
			for (int j = 0; j < matBCols; j++)
			{
				for (int k = 0; k < matACols; k++)
				{
					result[i, j] += matA[i, k] * matB[k, j];
				}
			}
		}

		s.Stop();
		RunTime = s.Elapsed;
	}
}

We need to share the values between the two models we have for demonstrating matrices multiplication. As such I’ve decided to put these values in a class that extends Sample and then have the second model extend this class. We’re using the Lazy<> class to initialize our matrices. Because of that I don’t start the stopwatch until after the matrices have been retrieved so as not to affect the time. Since we’re not using an image the ImageRequired returns false.

MatricesMultiplicationParallelSample:

public class MatricesMultiplicationParallelSample : MatricesMultiplicationSample
{
	public override string SampleName
	{
		get { return "Matrices Multiplication Parallel"; }
	}

	public override void Run(System.Drawing.Bitmap bmp = null, Action<string> updateLog = null)
	{
		double[,] matA = Matrix1;
		double[,] matB = Matrix2;
		double[,] result = new double[rowCount, colCount2];

		Stopwatch s = new Stopwatch();
		s.Start();

		int matACols = matA.GetLength(1);
		int matBCols = matB.GetLength(1);
		int matARows = matA.GetLength(0);

		// A basic matrix multiplication.
		// Parallelize the outer loop to partition the source array by rows.
		Parallel.For(0, matARows, i =>
		{
			for (int j = 0; j < matBCols; j++)
			{
				double temp = 0;
				for (int k = 0; k < matACols; k++)
				{
					temp += matA[i, k] * matB[k, j];
				}
				result[i, j] = temp;
			}
		});

		s.Stop();
		RunTime = s.Elapsed;
	}
}

For the second model, the only big difference is replacing the first outer loop with a Parallel.For. I’ve also overridden the SampleName but other than that it just uses values it inherited from the base.

What is really exciting to me is that to add a new sample there are only two places you have to modify.

SamplerViewMode.ctor:

public SamplerViewModel()
{
	Samples = new ObservableCollection();
	Sampler = new Sampler();
	Sampler.Samples.Add(new LineSample());
	Sampler.Samples.Add(new LineParallelSample());
	Sampler.Samples.Add(new GreyScaleSample());
	Sampler.Samples.Add(new GreyScaleParallelSample());
	Sampler.Samples.Add(new GreyScaleDoubleParallelSample());
	Sampler.Samples.Add(new MatricesMultiplicationSample());
	Sampler.Samples.Add(new MatricesMultiplicationParallelSample());
	ResetSampler();
}

SamplerViewModelFactory.maps dictionary:

private static Dictionary<Type, Func<Sample, SampleViewModel>> maps = new Dictionary<Type, Func<Sample, SampleViewModel>>()
{
	{ typeof(LineSample), (q) => new SampleViewModel((Sample)q)},
	{ typeof(LineParallelSample), (q) => new SampleViewModel((Sample)q)},
	{ typeof(GreyScaleSample), (q) => new SampleViewModel((Sample)q)},
	{ typeof(GreyScaleParallelSample), (q) => new SampleViewModel((Sample)q)},
	{ typeof(GreyScaleDoubleParallelSample), (q) => new SampleViewModel((Sample)q)},
	{ typeof(MatricesMultiplicationSample), (q) => new SampleViewModel((Sample)q)},
	{ typeof(MatricesMultiplicationParallelSample), (q) => new SampleViewModel((Sample)q)}
};

In the first code sample I add an instance of each of the two new models to the constructor of our ViewModel. In the second code sample I add a mapping of the sample type to the view model.

And that is it. Because we’re using the same ViewModel and View for these two models adding them is easy. Now binding just takes care of wiring everything up.

Starting Matrices Multiplication
Completed Matrices Multiplication
Matrices Multiplication ran in 00:00:01.8307101

Starting Matrices Multiplication Parallel
Completed Matrices Multiplication Parallel
Matrices Multiplication Parallel ran in 00:00:00.3797019

As with our other samples, where we give the parallel loops enough work we get significant benefits when using the TPL and Parallel.For, nearly 5 times faster in my case, though as always you may see better or worse times based on your situation.

In the next post we’ll go over a sample that shows how to handle exceptions in a Parallel.For and Parallel.ForEach using AggregateException.

Thanks,
Brian

Leave a Reply