Подтвердить что ты не робот

Помогите понять оптимизацию С#

Я играл с С# и хотел ускорить программу. Я сделал изменения и смог это сделать. Тем не менее, мне нужна помощь, чтобы понять, почему изменения сделали это быстрее.

Я попытался свести код к чему-то более понятному в вопросе. Показатели 1 и Report1 являются более медленными. Score2 и Report2 - это более быстрый способ. Первый метод сначала хранит строку и int в структуре параллельно. Затем в последовательном цикле он проходит через массив этих структур и записывает свои данные в буфер. Второй метод сначала записывает данные в строковый буфер параллельно. Затем в последовательном цикле он записывает строковые данные в буфер. Вот несколько примеров времени выполнения:

Запуск 1 Общее среднее время = 0,492087 с Run 2 Общее среднее время = 0,273619 sec

Когда я работал с более ранней непараллельной версией этого, времена были почти такими же. Почему разница с параллельной версией?

Даже если я уменьшу цикл в Report1, чтобы записать одну строчку вывода в буфер, он все еще медленнее (общее время около 0,42 секунды).

Вот упрощенный код:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Diagnostics;
using System.Threading.Tasks;
using System.IO;

namespace OptimizationQuestion
{
    class Program
    {
        struct ValidWord
        { 
            public string word;
            public int score;
        }
        ValidWord[] valid;
        StringBuilder output;
        int total; 

        public void Score1(string[] words)
        {
            valid = new ValidWord[words.Length];

            for (int i = 0; i < words.Length; i++)
            {
                StringBuilder builder = new StringBuilder();

                foreach (char c in words[i])
                {
                    if (c != 'U')
                        builder.Append(c);
                }
                if (words[i].Length == 3)
                {
                    valid[i] = new ValidWord 
                    { word = builder.ToString(), score = words[i].Length };
                }
            }
        }
        public void Report1(StringBuilder outputBuffer)
        {
            int total = 0;
            foreach (ValidWord wordInfo in valid)
            {
                if (wordInfo.score > 0)
                {
                    outputBuffer.AppendLine(String.Format("{0} {1}", wordInfo.word.ToString(), wordInfo.score));
                    total += wordInfo.score;
                }
            }
            outputBuffer.AppendLine(string.Format("Total = {0}", total));
        }

        public void Score2(string[] words)
        {
            output = new StringBuilder();
            total = 0;           
            for (int i = 0; i < words.Length; i++)
            {
                StringBuilder builder = new StringBuilder();

                foreach (char c in words[i])
                {
                    if (c != 'U')
                        builder.Append(c);
                }
                if (words[i].Length == 3)
                {
                    output.AppendLine(String.Format("{0} {1}", builder.ToString(), words[i].Length));
                    total += words[i].Length;
                }
            }
        }
        public void Report2(StringBuilder outputBuffer)
        {
            outputBuffer.Append(output.ToString());
            outputBuffer.AppendLine(string.Format("Total = {0}", total));
        } 
        static void Main(string[] args)
        {
            Program[] program = new Program[100];
            for (int i = 0; i < program.Length; i++)
                program[i] = new Program(); 

            string[] words = File.ReadAllLines("words.txt");

            Stopwatch stopwatch = new Stopwatch();
            const int TIMING_REPETITIONS = 20;
            double averageTime1 = 0.0;
            StringBuilder output = new StringBuilder();
            for (int i = 0; i < TIMING_REPETITIONS; ++i)
            {
                stopwatch.Reset();
                stopwatch.Start();
                output.Clear();
                Parallel.ForEach<Program>(program, p =>
                    {
                        p.Score1(words);
                    });
                for (int k = 0; k < program.Length; k++)
                    program[k].Report1(output);
                stopwatch.Stop();
                averageTime1 += stopwatch.Elapsed.TotalSeconds;
                GC.Collect();
            }
            averageTime1 /= (double)TIMING_REPETITIONS;
            Console.WriteLine(string.Format("Run 1 Total Average Time = {0:0.000000} sec", averageTime1));
            double averageTime2 = 0.0;
            for (int i = 0; i < TIMING_REPETITIONS; ++i)
            {
                stopwatch.Reset();
                stopwatch.Start();
                output.Clear();
                Parallel.ForEach<Program>(program, p =>
                    {
                        p.Score2(words);
                    });
                for (int k = 0; k < program.Length; k++)
                    program[k].Report2(output);
                stopwatch.Stop();
                averageTime2 += stopwatch.Elapsed.TotalSeconds;
                GC.Collect();
            }
            averageTime2 /= (double)TIMING_REPETITIONS;
            Console.WriteLine(string.Format("Run 2 Total Average Time = {0:0.000000} sec", averageTime2));
            Console.ReadLine();
        }
    }
}
4b9b3361

Ответ 1

Прежде всего, вы распараллеливаете повторяющиеся прогоны. Это улучшит ваше контрольное время, но может не очень помочь вашему реальному времени работы. Чтобы точно измерить, сколько времени потребуется для фактического прохождения через один список слов, вам нужно иметь ровно один список слов, идущий за раз. В противном случае отдельные потоки, обрабатывающие все списки, в какой-то степени конкурируют друг с другом за системные ресурсы и время, затрачиваемое на список, даже если время выполнения всех списков в целом происходит быстрее.

Чтобы ускорить процесс обработки одного списка слов, вы хотите обрабатывать отдельные слова в списке параллельно, точно для одного списка за раз. Чтобы получить достаточное определение/размер для хорошего измерения, либо сделайте список очень длинным, либо обработайте список много раз в последовательном порядке.

В вашем случае это немного сложно, потому что стробоструйщик, необходимый для вашего конечного продукта, не документируется как потокобезопасный. Это не так уж плохо. Здесь приведен пример вызова параллельного foreach для одного списка слов:

var locker = new Object(); //I'd actually make this static, but it should end up as a closure and so still work
var OutputBuffer = new StringBuilder(); // you can improve things futher if you can make a good estimate for the final size and force it allocate all the memory it will need up front
int score = 0;
Parallel.ForEach(words, w => 
{
   // We want to push as much of the work to the individual threads as possible.
   // If run in 1 thread, a stringbuilder per word would be bad.
   // Run in parallel, it allows us to do a little more of the work outside of locked code.
   var buf = new StringBuilder(w.Length + 5);
   string word = buf.Append(w.Where(c=>c!='U').Concat(' ').ToArray()).Append(w.Length).ToString();

   lock(locker)
   {
       OutputBuffer.Append(word);
       score += w.Length;
   }
});
OutputBuffer.Append("Total = ").Append(score);

Просто вызовите это 20 раз в обычном последовательно обработанном для цикла. Опять же, это может привести к замедлению тестов, но я думаю, что он будет работать в реальном мире немного быстрее из-за недостатка в вашем тесте. Также обратите внимание, что я набрал это право в окне ответа — Я никогда не делал попыток скомпилировать его, и поэтому он вряд ли будет идеальным из ворот.

После того, как вы установили свой тест, чтобы более точно отразить, как параллельный код повлияет на ваше время обработки в реальном времени, следующий шаг - сделать некоторые профилирование, чтобы увидеть, где ваша программа действительно тратит время. Вот как вы знаете, какие области следует искать для улучшения.

Из любопытства я также хотел бы знать, как эта версия выполняет:

var agg = new {score = 0, OutputBuffer = new StringBuilder()};
agg = words.Where(w => w.Length == 3)
   .Select(w => new string(w.Where(c => c!='U').ToArray())
   .Aggregate(agg, (a, w) => {a.OutputBuffer.AppendFormat("{0} {1}\n", w, w.Length); score += w.Length;});
agg.OutputBuffer.Append("Total = ").Append(score);

Ответ 2

Размер структуры обычно должен быть меньше, чем указатель (если производительность является основной проблемой. Microsoft говорит, что что-то меньшее, чем 16 байт лучше работает в качестве структуры, если не требуется семантика ссылочного типа), иначе накладные расходы для ее передачи возрастают (потому что это пропуск по значению) и будут больше, чем просто для передачи указателя. Из-за этого ваша структура содержит указатель и int (что делает его более чем указателем), поэтому из-за этого вы будете испытывать накладные расходы.

См. раздел "Когда использовать раздел" в этой статье.

Ответ 3

Я попытался запустить его через профилировщик, но я не доверяю полученным результатам. (Run1 занимает меньше времени, чем Run2 в нем.) Таким образом, нет конкретных ответов там, но мое подозрение в том, что допустимый массив [] является виновником:

  • Это потенциально большое выделение памяти, которое Run1 делает, а Run2 - нет. Выделение больших блоков памяти может занять много времени.

  • Возможно, что массив заканчивается далеко от любых других рабочих данных в физической памяти. По крайней мере, он достаточно большой, чтобы попасть в кучу больших объектов, тогда как похоже, что все остальное будет в конечном итоге на стеке или кучке небольших объектов. Это может означать, что функции Score1 приходится иметь дело с большим количеством промахов в кеше, чем функция Score2.

Это может быть намного меньше в серийном коде, где вы делаете это только один раз в любой момент времени. Однако, когда это происходит для большого количества потоков одновременно, проблема может быть сложной, так что из-за того, что изначально только что вызвало дополнительную промаху в кэше или два, теперь возникают ошибки страницы.

Ответ 4

Итак, есть сообщение в кодепероте, которое помогает ответить на этот вопрос.

http://www.codeproject.com/KB/cs/foreach.aspx

Там вы увидите, что сгенерированный код немного отличается, поэтому в длинном списке вы потеряете несколько cicles для этих лишних строк, и это изменит окончательное время.

Ответ 5

Ну, я только что просмотрел ваш код, и мои первые мысли - это время действия. В вашем Score1 вы выполняете новое распределение памяти для каждого прогона

valid[i] = new ValidWord 

это, в свою очередь, позволяет обнаружению памяти процесса приложения, а затем либо инициализирует его, либо создает новый блок памяти, устанавливает значения и копии по вновь созданному блоку в исходное местоположение (я забываю, что, а не точка).

То, что я пытаюсь сделать, - это то, что теперь вы требуете от приложения делать 14000 операций чтения/записи памяти, каждый из которых занимает х (микро) секунд. И если новая память выделяется, ей необходимо найти правильные размеры разделов памяти.

Анализ эффективности кода - довольно широкая тема, и я полагаю, что только встроенные программисты действительно используют это на ежедневной основе. Просто помните, что каждое сделанное вами выражение имеет операции, связанные с ним. Например, чтение Vector<bool> и Vector<int>, bool будет иметь более медленное время чтения, потому что ему необходимо разделить память на биты, а затем вернуть значение, где int может извлекать более крупные фрагменты памяти.

Хорошо, что мои 2 цента стоят, надеюсь, это даст вам лучшее представление о том, что искать. У меня есть хорошая книга, в которой рассказывается о том, как анализировать ваши строки кода и какие моменты времени он будет использовать. увидит, могу ли я удержать его (недавно перенесенный) и обновить имя для вас.

Ответ 6

Какова цель программы? Score1 и Score2 ничего нам не говорят о том, что пытается сделать алгоритм. Похоже, что он пытается найти любое слово, которое состоит из трех букв со всей капиталом. U удалено, является допустимым словом и добавляется в список?

Какова точка вызова Parallel.Foreach в кучке экземпляров программы, когда каждому передается тот же самый вход? И всегда создание StringBuilder для каждого слова не является хорошим подходом. Вы хотите свести к минимуму любые новые вызовы в критически важных для производительности областях, чтобы уменьшить количество раз, когда GC должен вступить.

Я запустил вашу программу в текстовом файле: http://introcs.cs.princeton.edu/data/words.txt

  • Выполнить 1 Общее среднее время = 0.160149 sec
  • Run 2 Общее среднее время = 0,056846 sec

Запуск в профиле прокси-сервера VS 2010 показывает, что Report1 примерно на 78 раз медленнее, чем Report2, и учитывает большую часть разницы. В основном из-за всех вызовов string.Format и Append.

Score1 и Score2 примерно одинаковы по скорости, поскольку Score1 идет немного медленнее из-за дополнительного времени в StringBuilder.ctor и clr.dll.

Но я подозреваю, что ваш алгоритм можно было бы переписать, если бы все строковые сборки или распределения не были на порядок быстрее.

Ответ 7

Просто идея: я не сделал никаких измерений, но, например, эту строку:

foreach (char c in words[i])
  • Я думаю, было бы лучше создать временную переменную для текущего слова.

  • Итератор строки может быть медленнее.

Код будет выглядеть примерно так:

var currentWord = words[i];
for (int j = 0; j < currentWord.Length; j++){
    char c = currentWord[i]; 
    // ...
}

Новый может также быть проблемой производительности, поскольку кто-то уже выявил. Как я уже сказал в своем комментарии, добавление дополнительных профилирующих данных поможет точно указать, что происходит. Или посмотрите на сгенерированный код.