Подобно функции Explode (), предлагаемой Mef, но с несколькими отличиями (один из которых я считаю исправлением ошибок):
type
TArrayOfString = array of String;
function SplitString(const aSeparator, aString: String; aMax: Integer = 0): TArrayOfString;
var
i, strt, cnt: Integer;
sepLen: Integer;
procedure AddString(aEnd: Integer = -1);
var
endPos: Integer;
begin
if (aEnd = -1) then
endPos := i
else
endPos := aEnd + 1;
if (strt < endPos) then
result[cnt] := Copy(aString, strt, endPos - strt)
else
result[cnt] := '';
Inc(cnt);
end;
begin
if (aString = '') or (aMax < 0) then
begin
SetLength(result, 0);
EXIT;
end;
if (aSeparator = '') then
begin
SetLength(result, 1);
result[0] := aString;
EXIT;
end;
sepLen := Length(aSeparator);
SetLength(result, (Length(aString) div sepLen) + 1);
i := 1;
strt := i;
cnt := 0;
while (i <= (Length(aString)- sepLen + 1)) do
begin
if (aString[i] = aSeparator[1]) then
if (Copy(aString, i, sepLen) = aSeparator) then
begin
AddString;
if (cnt = aMax) then
begin
SetLength(result, cnt);
EXIT;
end;
Inc(i, sepLen - 1);
strt := i + 1;
end;
Inc(i);
end;
AddString(Length(aString));
SetLength(result, cnt);
end;
Различия:
Примеры:
SplitString(':', 'abc') returns : result[0] = abc
SplitString(':', 'a:b:c:') returns : result[0] = a
result[1] = b
result[2] = c
result[3] = <empty string>
SplitString(':', 'a:b:c:', 2) returns: result[0] = a
result[1] = b
Это конечный разделитель и условный «пустой конечный элемент», который я считаю исправлением ошибки.
Я также включил изменение распределения памяти, которое я предложил, с уточнением (я ошибочно предположил, что входная строка может содержать не более 50% разделителей, но вполне возможно, что она состоит из 100% разделительных строк, что дает массив пустых элементов!) [/ g7]
Первый: Прекратить использование DateTime для измерения времени выполнения. Вместо этого используйте секундомер. Код теста будет выглядеть так:
var watch = new Stopwatch();
var strList = Enumerable.Repeat(10, 10000000);
watch.Start();
var result = strList.Sum();
watch.Stop();
Console.WriteLine("Linear: {0}", watch.ElapsedMilliseconds);
watch.Reset();
watch.Start();
var parallelResult = strList.AsParallel().Sum();
watch.Stop();
Console.WriteLine("Parallel: {0}", watch.ElapsedMilliseconds);
Console.ReadKey();
Второй: Параллельный запуск чего-либо увеличивает накладные расходы. В этом случае PLINQ должен найти лучший способ разделить вашу коллекцию, чтобы можно было безопасно суммировать элементы параллельно. После этого вам нужно объединить результаты из различных созданных потоков и просуммировать их. Это нетривиальная задача.
Используя приведенный выше код, я вижу, что использование Sum () приводит к вызову ~ 95 мсек. Вызов .AsParallel (). Sum () возвращает около 185 мс.
Выполнение задачи в параллельном режиме - хорошая идея, только если вы что-то выиграете, выполняя это. В этом случае Sum - достаточно простая задача, которую вы не получите с помощью PLINQ.
Это классическая ошибка - думать: «Я проведу простой тест, чтобы сравнить производительность этого однопоточного кода с этим многопоточным кодом».
A простой тест - это худший вид теста, который вы можете запустить для измерения многопоточной производительности.
Обычно распараллеливание некоторой операции дает выигрыш в производительности , когда распараллеливание шагов требует значительной работы . Когда шаги просты - например, быстро * - накладные расходы на распараллеливание вашей работы в конечном итоге затмевают ничтожный прирост производительности, который вы в противном случае получили бы.
Рассмотрим эту аналогию.
Вы строите здание. Если у вас есть один рабочий, он должен класть кирпичи один за другим, пока не построит одну стену, затем сделать то же самое для следующей стены и так далее, пока все стены не будут построены и соединены. Это медленная и трудоемкая задача, для которой может быть полезно распараллеливание.
Правильный способ сделать это - распараллелить здание стены - нанять, скажем, еще 3 рабочих, и пусть каждый рабочий построит свою стену так, чтобы 4 стены можно строить одновременно. Время, необходимое для поиска 3 дополнительных рабочих и назначения им их задач, несущественно по сравнению с экономией, которую вы получаете, возводя 4 стены за время, которое раньше требовалось бы для постройки 1.
неправильным способом сделать это было бы распараллеливать кладку кирпича - нанять еще около тысячи рабочих и поручить каждому рабочему укладывать по одному кирпичу за раз.Вы можете подумать: «Если один рабочий может уложить 2 кирпича в минуту, то тысяча рабочих сможет уложить 2000 кирпичей в минуту, так что я закончу эту работу в кратчайшие сроки!» Но реальность такова, что распараллеливая свою рабочую нагрузку на таком микроскопическом уровне, вы тратите огромное количество энергии, собирая и координируя всех своих сотрудников, поручив им задачи («положите этот кирпич прямо здесь»), следя за тем, чтобы никто не работа мешает работе других и т. д.
Итак, мораль этой аналогии такова: в общем, используйте распараллеливание, чтобы разделить существенные единицы работы (например, стены), но оставьте несущественные единицы (например, кирпичи) должны обрабатываться обычным последовательным образом.
* По этой причине вы действительно можете довольно хорошо оценить выигрыш в производительности от распараллеливания в более трудоемком контексте, взяв любой быстро выполняющийся код и добавив Thread.Sleep (100)
(или какое-то другое случайное число) до конца. Внезапно последовательное выполнение этого кода будет замедлено на 100 мс на итерацию, в то время как параллельное выполнение будет замедлено значительно меньше.
Другие указали на некоторые недостатки ваших тестов. Вот короткое консольное приложение для упрощения:
using System;
using System.Diagnostics;
using System.Linq;
public class Test
{
const int Iterations = 1000000000;
static void Main()
{
// Make sure everything's JITted
Time(Sequential, 1);
Time(Parallel, 1);
Time(Parallel2, 1);
// Now run the real tests
Time(Sequential, Iterations);
Time(Parallel, Iterations);
Time(Parallel2, Iterations);
}
static void Time(Func<int, int> action, int count)
{
GC.Collect();
Stopwatch sw = Stopwatch.StartNew();
int check = action(count);
if (count != check)
{
Console.WriteLine("Check for {0} failed!", action.Method.Name);
}
sw.Stop();
Console.WriteLine("Time for {0} with count={1}: {2}ms",
action.Method.Name, count,
(long) sw.ElapsedMilliseconds);
}
static int Sequential(int count)
{
var strList = Enumerable.Repeat(1, count);
return strList.Sum();
}
static int Parallel(int count)
{
var strList = Enumerable.Repeat(1, count);
return strList.AsParallel().Sum();
}
static int Parallel2(int count)
{
var strList = ParallelEnumerable.Repeat(1, count);
return strList.Sum();
}
}
Компиляция:
csc /o+ /debug- Test.cs
Результаты на моем ноутбуке с четырехъядерным процессором i7; работает до 2 ядер быстрее или до 4 ядер медленнее. Обычно побеждает ParallelEnumerable.Repeat
, за которым следует версия последовательности, за которой следует параллельное выполнение обычного Enumerable.Repeat
.
Time for Sequential with count=1: 117ms
Time for Parallel with count=1: 181ms
Time for Parallel2 with count=1: 12ms
Time for Sequential with count=1000000000: 9152ms
Time for Parallel with count=1000000000: 44144ms
Time for Parallel2 with count=1000000000: 3154ms
Обратите внимание, что в более ранних версиях этого ответа был досадный недостаток из-за неправильного количества элементов - я гораздо более уверен в результатах выше.
Возможно, вы не учитываете время JIT? Вам следует запустить тест дважды и отбросить первый набор результатов.
Кроме того, вы не должны использовать DateTime для определения времени производительности, вместо этого используйте класс Stopwatch
:
var swatch = new Stopwatch();
swatch.StartNew();
var strList = Enumerable.Repeat(10, repeatedCount);
var result = strList.AsParallel().Sum();
swatch.Stop();
textBox1.Text = swatch.Elapsed;
PLINQ добавляет некоторые накладные расходы на обработку последовательности. Но большая разница в вашем случае кажется чрезмерной. PLINQ имеет смысл, когда накладные расходы перевешиваются преимуществом запуска логики на нескольких ядрах / ЦП. Если у вас нет нескольких ядер, параллельная обработка не дает реальных преимуществ - и PLINQ должен обнаруживать такой случай и выполнять обработку последовательно.
РЕДАКТИРОВАТЬ: При создании встроенных тестов производительности такого типа вы должны убедиться, что вы не запускаете их под отладчиком или с включенным Intellitrace, так как это может значительно исказить время производительности.
Я бы рекомендовал использовать класс Stopwatch для метрик времени. В вашем случае это лучшая мера интервала.
Это действительно может иметь место, потому что вы увеличиваете количество переключений контекста и не выполняете никаких действий, которые могли бы принести пользу потокам, ожидающим чего-то вроде завершения ввода-вывода. Будет еще хуже, если вы работаете с одним процессором.
Прочтите раздел «Побочные эффекты» этой статьи.
http://msdn.microsoft.com/en-us/magazine/cc163329.aspx
Я думаю, вы можете столкнуться со многими условиями, когда PLINQ имеет дополнительные шаблоны обработки данных, которые вы должны понять, прежде чем вы решите думать, что это всегда будет иметь более быстрое время отклика.
Комментарий Джастина по поводу накладных расходов совершенно правильный.
Просто кое-что, что следует учитывать при написании параллельного программного обеспечения в целом, помимо использования PLINQ:
Вы всегда должны думать о «гранулярности» ваших рабочих элементов. Некоторые задачи очень хорошо подходят для распараллеливания, потому что они могут быть «разбиты» на очень высоком уровне, например, одновременная трассировка лучей целых кадров (такого рода проблемы называются до неприличия параллельными). Когда есть очень большие «порции» работы, накладные расходы на создание и управление несколькими потоками становятся незначительными по сравнению с фактической работой, которую вы хотите выполнить.
PLINQ упрощает параллельное программирование, но это не означает, что вы можете не думать о гранулярности своей работы.