如何在多线程应用程序中实现100%的CPU使用率?

时间:2017-06-02 08:28:48

标签: c# .net multithreading performance

我有~100个文本文件,每个200MB,我需要解析它们。下面的程序加载文件并并行处理它们。它可以为每个文件创建一个Thread或每个文件创建一个Process。问题是:如果我使用线程,它永远不会使用100%的CPU,并且需要更长的时间才能完成。

THREAD PER FILE
total time: 430 sec
CPU usage 15-20%
CPU frequency 1.2 GHz

PROCESS PER FILE
total time 100 sec
CPU usage 100%
CPU frequency 3.75 GHz

我正在使用带有HT的E5-1650 v3 Hexa-Core,因此我一次处理12个文件。

如何通过线程实现100%的CPU利用率?

以下代码不使用处理结果,因为它不会影响问题。

using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading;

namespace libsvm2tsv
{
    class Program
    {
        static void Main(string[] args)
        {
            var sw = Stopwatch.StartNew();

            switch (args[0])
            {
                case "-t": LoadAll(args[1], LoadFile); break;
                case "-p": LoadAll(args[1], RunChild); break;
                case "-f": LoadFile(args[1]); return;
            }

            Console.WriteLine("ELAPSED: {0} sec.", sw.ElapsedMilliseconds / 1000);
            Console.ReadLine();
        }

        static void LoadAll(string folder, Action<string> algorithm)
        {
            var sem = new SemaphoreSlim(12);
            Directory.EnumerateFiles(folder).ToList().ForEach(f=> {
                sem.Wait();
                new Thread(() => { try { algorithm(f); } finally { sem.Release(); } }).Start();
            });
        }

        static void RunChild(string file)
        {
            Process.Start(new ProcessStartInfo
            {
                FileName = Assembly.GetEntryAssembly().Location,
                Arguments = "-f \"" + file + "\"",
                UseShellExecute = false,
                CreateNoWindow = true
            })
            .WaitForExit();
        }

        static void LoadFile(string inFile)
        {
            using (var ins = File.OpenText(inFile))
                while (ins.Peek() >= 0)
                    ParseLine(ins.ReadLine());
        }

        static long[] ParseLine(string line)
        {
            return line
                .Split()
                .Skip(1)
                .Select(r => (long)(double.Parse(r.Split(':')[1]) * 1000))
                .Select(r => r < 0 ? -1 : r)
                .ToArray();
        }
    }
}

4 个答案:

答案 0 :(得分:1)

  

我有大约100个文本文件,每个200MB,我需要解析它们。

从旋转磁盘读取数据或向旋转磁盘写入数据的最快方法是按顺序最小化磁头磁头寻找数据或将其写入指定位置所需的时间。因此,对单个磁盘执行并行IO会降低IO速率 - 并且根据实际的IO模式,它可以显着降低速率。一个可以顺序处理100 MB /秒的磁盘,每秒只能移动20或30 千字节,可以对小块数据进行并行读/写。

如果我优化了这样一个过程,我不会首先担心CPU利用率,而是首先优化IO吞吐量。除非您正在进行一些非常耗费CPU的解析,否则您将受到IO限制。一旦优化了IO吞吐量,如果您获得100%的CPU利用率,那么您将受到CPU限制。如果您的设计可以很好地扩展,那么您可以添加CPU并且可能运行得更快。

要加速IO,首先需要尽量减少磁盘搜索,特别是如果您使用的是消费级廉价的SATA硬盘。有多种方法可以做到这一点。

首先,最简单的 - 消除磁头。将您的数据放在SSD上。问题解决了,无需编写复杂的,容易出错的优化代码。使用软件使运行速度更快需要多长时间?你必须设计一些东西,测试它,调整它,调试它,更重要的是,让它保持运行并运行良好。这些都不是免费的。一个重要的成本是花费时间使事情变得更快的机会成本 - 当你这样做时,你并没有解决任何其他问题。更快的硬件没有这些成本。在这种情况下,购买SSD,插入它们,你就会更快。

但如果您真的想花几周或更长时间来优化处理软件,请按照以下方式进行操作:

  1. 将数据传播到多个磁盘上。您无法快速对物理磁盘执行并行IO,因为磁头搜索会破坏性能。因此尽可能多地读取和写入不同的磁盘。
  2. 对于每个磁盘,都有一个读取器或写入器线程或进程,用于将数据提供给工作池或写入该工作池提供的数据。
  3. 可调整数量的工作线程/进程以进行实际解析。
  4. 这样,您可以随后读取文件并写入输出数据,而不会在其他IO进程的每个磁盘上发生争用。

答案 1 :(得分:1)

最后,我找到了瓶颈。我使用string.Split来解析每行数据中的数字,所以我得到了数十亿字符串。这些字符串放在堆中。由于所有线程共享单个堆内存分配是同步的。由于进程有单独的堆 - 没有同步发生,事情很快。这是问题的根源。因此,我使用IndexOf而不是Split重写了解析,并且线程开始执行甚至比单独的进程更好。就像我预期的那样。

由于.NET没有默认工具来解析字符串中某个位置的实数,我使用了这个:https://codereview.stackexchange.com/questions/75791/optimize-custom-double-parse,修改很少。

using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;

namespace libsvm2tsv
{
    class Program
    {

        static void Main(string[] args)
        {
            var sw = Stopwatch.StartNew();

            switch (args[0])
            {
                case "-t": LoadAll(args[1], LoadFile); break;
                case "-p": LoadAll(args[1], RunChild); break;
                case "-f": LoadFile(args[1]); return;
            }

            Console.WriteLine("ELAPSED: {0} sec.", sw.ElapsedMilliseconds / 1000);
            Console.ReadLine();
        }

        static void LoadAll(string folder, Action<string> algorithm)
        {
            Parallel.ForEach(
                Directory.EnumerateFiles(folder),
                new ParallelOptions { MaxDegreeOfParallelism = 12 },
                f => algorithm(f));
        }

        static void RunChild(string file)
        {
            Process.Start(new ProcessStartInfo
            {
                FileName = Assembly.GetEntryAssembly().Location,
                Arguments = "-f \"" + file + "\"",
                UseShellExecute = false,
                CreateNoWindow = true
            })
            .WaitForExit();
        }

        static void LoadFile(string inFile)
        {
            using (var ins = File.OpenText(inFile))
                while (ins.Peek() >= 0)
                    ParseLine(ins.ReadLine());
        }

        static long[] ParseLine(string line)
        {
            // first, count number of items
            var items = 1;
            for (var i = 0; i < line.Length; i++)
                if (line[i] == ' ') items++;

            //allocate memory and parse items
            var all = new long[items];
            var n = 0;
            var index = 0;
            while (index < line.Length)
            {
                var next = line.IndexOf(' ', index);
                if (next < 0) next = line.Length;
                if (next > index)
                {
                    var v = (long)(parseDouble(line, line.IndexOf(':', index) + 1, next - 1) * 1000);
                    if (v < 0) v = -1;
                    all[n++] = v;

                }
                index = next + 1;
            }

            return all;
        }

        private readonly static double[] pow10Cache;
        static Program()
        {
            pow10Cache = new double[309];

            double p = 1.0;
            for (int i = 0; i < 309; i++)
            {
                pow10Cache[i] = p;
                p /= 10;
            }
        }

        static double parseDouble(string input, int from, int to)
        {
            long inputLength = to - from + 1;
            long digitValue = long.MaxValue;
            long output1 = 0;
            long output2 = 0;
            long sign = 1;
            double multiBy = 0.0;
            int k;

            //integer part
            for (k = 0; k < inputLength; ++k)
            {
                digitValue = input[k + from] - 48; // '0'

                if (digitValue >= 0 && digitValue <= 9)
                {
                    output1 = digitValue + (output1 * 10);
                }
                else if (k == 0 && digitValue == -3 /* '-' */)
                {
                    sign = -1;
                }
                else if (digitValue == -2 /* '.' */ || digitValue == -4 /* ',' */)
                {
                    break;
                }
                else
                {
                    return double.NaN;
                }
            }

            //decimal part
            if (digitValue == -2 /* '.' */ || digitValue == -4 /* ',' */)
            {
                multiBy = pow10Cache[inputLength - (++k)];

                for (; k < inputLength; ++k)
                {
                    digitValue = input[k + from] - 48; // '0'

                    if (digitValue >= 0 && digitValue <= 9)
                    {
                        output2 = digitValue + (output2 * 10);
                    }
                    else
                    {
                        return Double.NaN;
                    }
                }

                multiBy *= output2;
            }

            return sign * (output1 + multiBy);
        }
    }
}

答案 2 :(得分:0)

我会考虑用Parallel.ForEach替换ForEach并删除你明确使用的Threads。使用https://stackoverflow.com/a/5512363/34092设置要将其限制为的线程数。

static void LoadAll(string folder, Action<string> algorithm)
{
    Parallel.ForEach(Directory.EnumerateFiles(folder), algorithm);
}

答案 3 :(得分:0)

正如其他人所说,IO最终可能成为瓶颈,并且100%的CPU使用率实际上是无关紧要的。我觉得他们遗漏了一些东西:你的流程吞吐量高于线程,这意味着IO不是唯一的瓶颈。原因是CPU在进程中以更高的频率运行,并且您希望它在不等待IO时以峰值速度运行!那么,你怎么能这样做呢?

最简单的方法是手动设置电源选项的电源配置文件。编辑电源选项并将最小和最大处理器状态设置为100%。那应该可以胜任。

如果您想从您的计划中执行此操作,请查看How to Disable Dynamic Frequency Scaling?。 .NET可能没有使用本机代码,但我现在无法找到它。