Click here to Skip to main content
15,885,366 members
Articles / Programming Languages / CUDA

High Performance Queries: GPU vs. PLINQ vs. LINQ

Rate me:
Please Sign up or sign in to vote.
4.94/5 (102 votes)
16 Sep 2013LGPL310 min read 143.9K   5K   195  
How to get 30x performance increase for queries by using your Graphics Processing Unit (GPU) instead of LINQ and PLINQ.
/*
CUDAfy.NET - LGPL 2.1 License
Please consider purchasing a commerical license - it helps development, frees you from LGPL restrictions
and provides you with support.  Thank you!
Copyright (C) 2011 Hybrid DSP Systems
http://www.hybriddsp.com

This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.

This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
*/
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using Cudafy;
using Cudafy.Host;
using Cudafy.Translator;
using Hybrid.DSP.GPS;
namespace AcceleratingQueriesUsingGPU
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
            nudNoTargets.Maximum = Track.ciMAXTARGETPOINTS;
        }

        private const string csNAME = "AcceleratingQueriesUsingGPU";

        private TrackPoint[] _track = new TrackPoint[0];
       
        private List<TrackPoint> _targets = new List<TrackPoint>();

        #region GPU

        private Track _trackQuery;

        /// <summary>
        /// Loads the track on to the GPU.
        /// </summary>
        private void LoadTrack()
        {
            if (_trackQuery == null)
                _trackQuery = new Track();
            Stopwatch sw = Stopwatch.StartNew();
            _trackQuery.LoadTrack(_track);
            long time = sw.ElapsedMilliseconds;
            SetStatusStrip(string.Format("Track uploaded to GPU in {0}ms", time));
        }

        private void SetStatusStrip(string text)
        {
            Invoke(new Action(() => { statusStrip.Items[0].Text = text; }));
        }

        private void btnTestGPU_Click(object sender, EventArgs e)
        {
            try
            {
                TestGPU(sender == btnRunOnGPUNaive);
            }
            catch (Exception ex)
            {
                MessageBox.Show(ex.Message);
            }
        }

        private void TestGPU(bool naive)
        {
            var startTime = dtpStartTime.Value;
            var endTime = dtpEndTime.Value;
            // If there are no target points or the parameters have changed then make targets.
            if (_targets.Count == 0 || _targetChanged)
                GenerateTargets();
            // Get the radius in metres around each target.
            double radius = Convert.ToSingle(tbRadius.Text);

            // Time the selecting and counting of the points that lie with radius of the targets.
            Stopwatch sw = Stopwatch.StartNew();
            var pnts = _trackQuery.SelectPoints(_targets.ToArray(), radius, startTime, endTime, naive);
            int cnt = pnts.Count();
            long time = sw.ElapsedMilliseconds;
            string ts = string.Format("Found {0} points in {1}ms using GPU", cnt, time);
            SetStatusStrip(ts);
            MessageBox.Show(string.Format("Searched {0} points with {1} targets.\r\n{2}", _track.Length, _targets.Count, ts));
        }

        #endregion

        #region Generate test route

        public void GenerateRoute()
        {
            int noPoints = (int)Invoke(new Func<int>(() => { return Convert.ToInt32(tbNoPoints.Text); }));
            if(noPoints < 1 || noPoints > 10000000)
                throw new ArgumentOutOfRangeException("Number of points must be between 1 and 10000000.");
            _track = new TrackPoint[noPoints];

            // Create a starting point at 5 degrees east and 45 degrees north. It will be a random route, so 
            // we set some values for generating this: max speed, current speed, current bearing.
            var tp = new TrackPoint(5, 45, DateTime.Now.Ticks);// longitute, latitude, timestamp in ticks
            double maxSpeed = _randSpeed.Next(30) + 50;
            double currentSpeed = GetNewSpeed(_randSpeed.Next((int)maxSpeed), maxSpeed); // m/s
            double bearing = _randSpeed.NextDouble() * 360;
            double w = 0, e=0, n=0, s=0;
            for (int i = 0; i < noPoints; i++)
            {
                int clockwiseCtr = _randSpeed.Next(30) + 5;
                // Get the next point at current bearing, speed and one second later (time is in ticks).
                tp = tp.GetGreatCircleCoordinateAt(bearing, currentSpeed, 10000000);
                // For the record let's keep track of the bounds of the track (max west, east, north, south).
                if (i == 0)
                {
                    n = tp.Latitude;
                    w = tp.Longitude;
                    e = tp.Longitude;
                    s = tp.Latitude;
                }
                else
                {
                    if (tp.Longitude < w)
                        w = tp.Longitude;
                    else if (tp.Longitude > e)
                        e = tp.Longitude;
                    if (tp.Latitude < s)
                        s = tp.Latitude;
                    else if (tp.Latitude > n)
                        n = tp.Latitude;
                }
                // Add point
                _track[i] = tp;
                // Calculate new bearing and speed based on current settings.
                bearing = GetNewBearing(bearing, currentSpeed, maxSpeed, i % clockwiseCtr == 0);
                currentSpeed = GetNewSpeed(currentSpeed, maxSpeed);
            }
            // Update the target generation settings in the UI.
            var startTime = _track[0].Time;
            var endTime = _track[_track.Length - 1].Time;

            Invoke(new Action(() => { dtpStartTime.Value = startTime; }));

            Invoke(new Action(() => { dtpEndTime.Value = endTime; }));
            
            Invoke(new Action(() => { lblTimeRange.Text = string.Format("Time range: {0} -> {1}", startTime, endTime); }));

            string areaStr = string.Format("{0},{1} -> {2},{3}", w.ToString("0.0"), n.ToString("0.0"), e.ToString("0.0"), s.ToString("0.0"));
            Invoke(new Action(() => { lblAreaRange.Text = string.Format("Area range: {0}", areaStr); }));
        }


        private Random _randSpeed = new Random();

        private bool accelerating = true;

        private double GetNewSpeed(double currentSpeed, double maxSpeed)
        {
            double ds = _randSpeed.NextDouble() * 5.0;
            if (!accelerating)
                ds *= -1;
            double newSpeed = currentSpeed + ds;
            if (newSpeed > maxSpeed)
                accelerating = false;
            if (newSpeed < 0)
            {
                accelerating = true;
                newSpeed = 0;
            }
            return newSpeed;
        }

        private double GetNewBearing(double currentBearing, double currentSpeed, double maxSpeed, bool clockwise)
        {
            double ds = currentSpeed > 0 ? _randSpeed.NextDouble() * (maxSpeed / currentSpeed) : 0;
            if (!clockwise)
                ds *= -1;
            double newBearing = currentBearing + ds;
            if (newBearing > 360)
                newBearing -= 360;
            else if(newBearing < 0)
                newBearing += 360;

            return newBearing;
        }

        private Action _action;

        private void btnGenerateRoute_Click(object sender, EventArgs e)
        {
            if (_action != null)
                return;
            _targetChanged = true;
            btnGenerateRoute.Enabled = false;
            btnRunOnGPU.Enabled = false;
            btnRunOnGPUNaive.Enabled = false;
            btnTestCPU.Enabled = false;
            btnPLINQ.Enabled = false;
            _action = new Action(GenerateRoute);
            _action.BeginInvoke(new AsyncCallback((res) =>
            {
                try
                {
                    _action.EndInvoke(res);

                    Invoke(new Action(() =>
                    {
                        // You need to initialize the GPU on the UI thread, unless we enable multithreading.
                        // Doing this on another thread will cause an invalid context error later.
                        // Let's keep things simple!
                        LoadTrack();
                    }));
                }
                catch (Exception ex)
                {
                    MessageBox.Show(ex.Message);
                    return;
                }
                finally
                {
                    Invoke(new Action(() =>
                    {
                        btnGenerateRoute.Enabled = true;
                        btnRunOnGPU.Enabled = true;
                        btnRunOnGPUNaive.Enabled = true;
                        btnPLINQ.Enabled = true;
                        btnTestCPU.Enabled = true;
                    }));
                    _action = null;
                }
                
            }), null);
        }

        /// <summary>
        /// Generates targets - simply select random points from the current route.
        /// </summary>
        private void GenerateTargets()
        {
            _targets.Clear();
            int noTargets = (int)nudNoTargets.Value;
            Random rand = new Random();

            for (int i = 0; i < noTargets; i++)
                _targets.Add(_track[rand.Next(_track.Length)]);
            _targetChanged = false;
        }

        private void btnGenerateTargets_Click(object sender, EventArgs e)
        {
            GenerateTargets();
        }

        private bool _targetChanged = true;

        private void dtpStartTime_ValueChanged(object sender, EventArgs e)
        {
            var dtp = sender as DateTimePicker;
            lblDate.Text = dtp.Value.ToShortDateString();
            _targetChanged = true;
        }

        private void dtpEndTime_ValueChanged(object sender, EventArgs e)
        {
            var dtp = sender as DateTimePicker;
            lblEndTime.Text = dtp.Value.ToShortDateString();
            _targetChanged = true;
        }

        private void tbRadius_TextChanged(object sender, EventArgs e)
        {
            _targetChanged = true;
        }

        private void nudNoTargets_ValueChanged(object sender, EventArgs e)
        {
            _targetChanged = true;
        }

        #endregion

        #region Test Linq and PLinq

        private void btnTestCPU_Click(object sender, EventArgs e)
        {
            TestCPU(sender != btnTestCPU);
        }

        /// <summary>
        /// Runs the query on the CPU.
        /// </summary>
        /// <param name="parallel">if set to <c>true</c> then use PLinq, else use Linq.</param>
        private void TestCPU(bool parallel)
        {
            var startTime = dtpStartTime.Value;
            var endTime = dtpEndTime.Value;
            if (_targets.Count == 0 || _targetChanged)
                GenerateTargets();

            double radius = Convert.ToSingle(tbRadius.Text);
            Stopwatch sw = Stopwatch.StartNew();
            var pnts = GetPoints(_targets.ToArray(), radius, startTime, endTime, parallel);
            int cnt = pnts.Count();
            long time = sw.ElapsedMilliseconds;
            string ts = string.Format("Found {0} points in {1}ms using {2}", cnt, time, parallel ? "PLINQ" : "LINQ");
            SetStatusStrip(ts);
            MessageBox.Show(string.Format("Searched {0} points with {1} targets.\r\n{2}", _track.Length, _targets.Count, ts));
        }

        private IEnumerable<TrackPoint> GetPoints(TrackPoint[] targets, double radius, DateTime startTime, DateTime endTime, bool parallel)
        {
            long targetStartTime = startTime.Ticks;
            long targetEndTime = endTime.Ticks;
            // Running the query in parallel is as easy as adding AsParallel(). We're not concerned with the ordering, so it's ideal.
            if(parallel)
                return _track.AsParallel().Where(tp => GetNearestTargetIndex(tp, targets, radius, targetStartTime, targetEndTime) < 255);
            else
                return _track.Where(tp => GetNearestTargetIndex(tp, targets, radius, targetStartTime, targetEndTime) < 255);
        }

        /// <summary>
        /// Gets the index of the nearest target.
        /// </summary>
        /// <param name="tp">The point in the track to test against.</param>
        /// <param name="targets">Array of targets.</param>
        /// <param name="radius">The radius.</param>
        /// <param name="targetStartTime">The target start time.</param>
        /// <param name="targetEndTime">The target end time.</param>
        /// <returns>Returns index of nearest target or 255 if none found.</returns>
        private byte GetNearestTargetIndex(TrackPoint tp, TrackPoint[] targets, double radius, long targetStartTime, long targetEndTime)
        {
            double minDist = Double.MaxValue;
            byte index = 255; 
            // If we're not within the time range then no need to look further.
            if (tp.TimeStamp >= targetStartTime && tp.TimeStamp <= targetEndTime)
            {
                int noTargets = targets.Length;
                // Go through the targets and get the index of the closest one.
                for (int i = 0; i < noTargets; i++)
                {
                    TrackPoint target = targets[i];
                    double d = tp.DistanceTo(target);
                    if (d <= radius && d < minDist)
                    {
                        minDist = d;
                        index = (byte)i;
                    }
                }
                }
            return index;
        }

        #endregion
    }
}

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

This article, along with any associated source code and files, is licensed under The GNU Lesser General Public License (LGPLv3)


Written By
Systems Engineer Hybrid DSP Systems
Netherlands Netherlands
Nick is co owner of Hybrid DSP, a company specialized in high speed data acquisition, processing and storage.

CUDAfy.NET took considerable effort to develop and we ask nothing in return from users of the LGPL library other than that you please consider donating to Harmony through Education. This small charity helps handicapped children in developing countries by providing suitable schooling.

Comments and Discussions