Чому «Де» та «Вибір» переважають просто Вибір?


145

У мене клас, як це:

public class MyClass
{
    public int Value { get; set; }
    public bool IsValid { get; set; }
}

Насправді це набагато більше, але це відтворює проблему (дивності).

Я хочу отримати суму Value, де дійсний екземпляр. Поки що я знайшов два рішення для цього.

Перший - це:

int result = myCollection.Where(mc => mc.IsValid).Select(mc => mc.Value).Sum();

Однак другий:

int result = myCollection.Select(mc => mc.IsValid ? mc.Value : 0).Sum();

Я хочу отримати найбільш ефективний метод. Я, по-перше, думав, що другий буде ефективнішим. Тоді теоретична частина мене почала йти: "Ну, один - O (n + m + m), другий - O (n + n). Перший повинен працювати краще, ніж більше інвалідів, а другий повинен працювати краще з меншою ». Я думав, що вони будуть виступати однаково. EDIT: І тоді @Martin вказував, що Де і Вибір поєднані, тому насправді це повинно бути O (m + n). Однак якщо подивитися нижче, здається, це не пов’язано.


Тому я поставив це на тест.

(Це 100+ рядків, тому я подумав, що краще опублікувати це як історію.)
Результати були ... цікаві.

З дотриманням 0% відхилення:

Ваги знаходяться в користь Selectі Where, приблизно ~ 30 балів.

How much do you want to be the disambiguation percentage?
0
Starting benchmarking.
Ties: 0
Where + Select: 65
Select: 36

З 2% відхиленням:

Це те саме, за винятком того, що для деяких вони були в межах 2%. Я б сказав, що це мінімальна помилка. Selectі Whereтепер маєте лише 20-кратне відрив.

How much do you want to be the disambiguation percentage?
2
Starting benchmarking.
Ties: 6
Where + Select: 58
Select: 37

З 5% -ним відхиленням:

Це я б сказав, що це моя максимальна помилка. Це робить його трохи краще для Select, але не набагато.

How much do you want to be the disambiguation percentage?
5
Starting benchmarking.
Ties: 17
Where + Select: 53
Select: 31

З дотриманням 10% відхилення:

Це вихід з моєї помилки, але мене все ще цікавить результат. Тому що це дає Selectі Whereдвадцять очок, які він мав на деякий час.

How much do you want to be the disambiguation percentage?
10
Starting benchmarking.
Ties: 36
Where + Select: 44
Select: 21

З дозволом на 25%:

Це шлях, шлях з мого краю помилки, але я все ще зацікавлений в результаті, тому що Selectі до Where сих пір (майже) тримати їх 20 очок. Здається, що це перевершує це на декількох кількох, і саме це дає йому перевагу.

How much do you want to be the disambiguation percentage?
25
Starting benchmarking.
Ties: 85
Where + Select: 16
Select: 0


Зараз я здогадуюсь, що 20-бальний відрив прийшов із середини, де вони обоє зобов'язані обійти одну й ту ж виставу. Я міг би спробувати ввійти, але це було б цілком завантажене інформацією. Графік був би кращим, я думаю.

Отже, це я і зробив.

Виберіть проти Вибрати та Де.

Це показує, що Selectлінія тримається стійкою (очікуваною) і що Select + Whereлінія піднімається вгору (очікується). Однак, що спантеличує мене, чому він не зустрінеться з Selectна 50 або вище: насправді я очікував раніше , ніж 50, в якості додаткового переписувач мав бути створений для SelectіWhere . Я маю на увазі, це показує 20-кратну перевагу, але це не пояснює, чому. Це, мабуть, головний момент мого питання.

Чому він поводиться так? Чи варто їй довіряти? Якщо ні, чи слід використовувати інший чи цей?


Як в коментарях згадував @KingKong, ви також можете використовувати Sum перевантаження, яке лямбда. Тож два мої варіанти тепер змінені на це:

Перший:

int result = myCollection.Where(mc => mc.IsValid).Sum(mc => mc.Value);

Друге:

int result = myCollection.Sum(mc => mc.IsValid ? mc.Value : 0);

Я зроблю його трохи коротше, але:

How much do you want to be the disambiguation percentage?
0
Starting benchmarking.
Ties: 0
Where: 60
Sum: 41
How much do you want to be the disambiguation percentage?
2
Starting benchmarking.
Ties: 8
Where: 55
Sum: 38
How much do you want to be the disambiguation percentage?
5
Starting benchmarking.
Ties: 21
Where: 49
Sum: 31
How much do you want to be the disambiguation percentage?
10
Starting benchmarking.
Ties: 39
Where: 41
Sum: 21
How much do you want to be the disambiguation percentage?
25
Starting benchmarking.
Ties: 85
Where: 16
Sum: 0

Двадцятибальний відрив все ще існує, це означає, що це не має відношення до WhereтаSelect комбінації вказували @Marcin в коментарях.

Дякуємо, що прочитали мою стінку тексту! Крім того, якщо вам цікаво, ось модифікована версія, що записує CSV, який приймає Excel.


1
Я б сказав, це залежить від того, наскільки дорога сума та доступ mc.Value.
Medinoc

14
@ ItNotALie. Where+ Selectне викликає двох відокремлених ітерацій над колекцією вхідних даних. LINQ to Objects оптимізує його в одну ітерацію. Детальніше читайте на моєму щоденнику
MarcinJuraszek

4
Цікаво. Дозвольте лише зазначити, що цикл для масиву буде в 10 разів швидшим, ніж найкраще рішення LINQ. Тож якщо ви йдете на полювання на перф, в першу чергу не використовуйте LINQ.
usr

2
Іноді люди запитують після реальних досліджень, це один приклад питання: я не користувач C # прийшов із списку гарячих питань.
Grijesh Chauhan

2
@WiSaGaN Це хороший момент. Однак, якщо це пов'язано з переходом від гілки до умовного, ми очікуємо, що найбільш драматична різниця буде на рівні 50% / 50%. Тут ми бачимо найдраматичніші відмінності на кінцях, де розгалуження найбільш передбачуване. Якщо гілка «Де», а потрійний - умовний хід, то ми очікуємо, що час, коли повернуться, коли всі елементи дійсні, але він ніколи не повертається назад.
Джон Ценг

Відповіді:


131

Select повторюється один раз по всьому набору і для кожного елемента виконує умовну гілку (перевірку на дійсність) та a + операцію.

Where+Selectстворює ітератор, який пропускає недійсні елементи (не yieldїх), виконуючи +лише валідні елементи.

Отже, вартість для Select:

t(s) = n * ( cost(check valid) + cost(+) )

І для Where+Select:

t(ws) = n * ( cost(check valid) + p(valid) * (cost(yield) + cost(+)) )

Де:

  • p(valid) - це ймовірність того, що елемент у списку дійсний.
  • cost(check valid) - це вартість філії, яка перевіряє чинність
  • cost(yield)- це вартість побудови нового стану whereітератора, який складніший за простий ітератор, який використовує Selectверсія.

Як ви можете бачити, для заданого n, то Selectверсія є постійною, в той час як Where+Selectверсія являє собою лінійне рівняння з в p(valid)якості змінної. Фактичні значення витрат визначають точку перетину двох ліній, і оскільки вони cost(yield)можуть відрізнятися від cost(+), вони не обов'язково перетинаються при p(valid)= 0,5.


34
+1 за те, що є єдиною відповіддю (поки що), яка насправді вирішує питання, не вгадує відповіді та не просто генерує "мене теж!" статистика.
Бінарний страшник

4
Технічно LINQ методи створюють дерева виразів, які виконуються по всій колекції один раз замість "наборів".
Спойк

Що cost(append)? Дійсно гарна відповідь, однак, дивиться на це під іншим кутом, а не просто зі статистикою.
ЦеNotALie.

5
Whereнічого не створює, просто повертайте один елемент одночасно з sourceпослідовності, якщо тільки він заповнює присудок.
MarcinJuraszek

13
@Spoike - Дерева виразів тут не доречні, тому що це linq-to-object , а не linkq-to-something-else (Entity, наприклад). Ось і є різниця між IEnumerable.Select(IEnumerable, Func)і IQueryable.Select(IQueryable, Expression<Func>). Ви маєте рацію, що LINQ не робить нічого, поки ви не повторите колекцію, що, мабуть, ви мали на увазі.
Кобі

33

Ось поглиблене пояснення того, що спричиняє різниці в часі.


Sum()Функція IEnumerable<int>виглядає наступним чином :

public static int Sum(this IEnumerable<int> source)
{
    int sum = 0;
    foreach(int item in source)
    {
        sum += item;
    }
    return sum;
}

У C # - foreachце лише синтаксичний цукор для версії ітератора .Net (не плутати з ним ) . Отже, наведений вище код насправді перекладається на це:IEnumerator<T> IEnumerable<T>

public static int Sum(this IEnumerable<int> source)
{
    int sum = 0;

    IEnumerator<int> iterator = source.GetEnumerator();
    while(iterator.MoveNext())
    {
        int item = iterator.Current;
        sum += item;
    }
    return sum;
}

Пам'ятайте, два рядки коду, які ви порівнюєте, наступні

int result1 = myCollection.Where(mc => mc.IsValid).Sum(mc => mc.Value);
int result2 = myCollection.Sum(mc => mc.IsValid ? mc.Value : 0);

Тепер ось кікер:

LINQ використовує відкладене виконання . Таким чином, хоча може здатися, що вона result1повторюється над колекцією двічі, вона насправді повторює лише один раз. Ця Where()умова реально застосовується під час Sum()дзвінка до MoveNext() (Це можливо завдяки магії yield return) .

Це означає, що, наприклад result1, код всередині whileциклу,

{
    int item = iterator.Current;
    sum += item;
}

виконується лише один раз для кожного елемента з mc.IsValid == true. Для порівняння, result2виконає цей код для кожного елемента колекції. Ось чому result1загалом швидше.

(Хоча зауважте, що виклик Where()умови в межах MoveNext()все ще має невеликі накладні витрати, тому, якщо більшість / всі елементи є mc.IsValid == true, result2насправді буде швидше!)


Сподіваємось, тепер зрозуміло, чому result2зазвичай повільніше. Тепер я хотів би пояснити, чому я зазначив у коментарях, що ці порівняння ефективності LINQ не мають значення .

Створення виразу LINQ - дешево. Виклик функцій делегата дешево. Розміщення та перекидання на ітераторі дешево. Але ще дешевше не робити цих речей. Таким чином, якщо ви виявите, що оператор LINQ є вузьким місцем у вашій програмі, за моїм досвідом перезапис його без LINQ завжди зробить це швидше, ніж будь-який з різних методів LINQ.

Отже, ваш робочий процес LINQ повинен виглядати приблизно так:

  1. Використовуйте LINQ скрізь.
  2. Профіль.
  3. Якщо профайлер каже, що причиною вузького місця є LINQ, перепишіть цей фрагмент коду без LINQ.

На щастя, вузькі місця LINQ рідкісні. Чорт, вузькі місця рідкісні. За останні кілька років я написав сотні заяв LINQ і замінив <1%. І більшість із них були наслідком поганої оптимізації SQL LINQ2EF , а не виною LINQ.

Отже, як завжди, спершу напишіть чіткий та розумний код і дочекайтеся, коли ви профілюєтесь, щоб потурбуватися про мікрооптимізацію.


3
Невеликий додаток: головна відповідь була виправлена.
ЦеNotALie.

16

Смішна річ. Чи знаєте ви, як це Sum(this IEnumerable<TSource> source, Func<TSource, int> selector)визначено? Він використовує Selectметод!

public static int Sum<TSource>(this IEnumerable<TSource> source, Func<TSource, int> selector)
{
    return source.Select(selector).Sum();
}

Так що насправді все повинно працювати майже так само. Я швидко зробив дослідження самостійно, і ось результати:

Where -- mod: 1 result: 0, time: 371 ms
WhereSelect -- mod: 1  result: 0, time: 356 ms
Select -- mod: 1  result 0, time: 366 ms
Sum -- mod: 1  result: 0, time: 363 ms
-------------
Where -- mod: 2 result: 4999999, time: 469 ms
WhereSelect -- mod: 2  result: 4999999, time: 429 ms
Select -- mod: 2  result 4999999, time: 362 ms
Sum -- mod: 2  result: 4999999, time: 358 ms
-------------
Where -- mod: 3 result: 9999999, time: 441 ms
WhereSelect -- mod: 3  result: 9999999, time: 452 ms
Select -- mod: 3  result 9999999, time: 371 ms
Sum -- mod: 3  result: 9999999, time: 380 ms
-------------
Where -- mod: 4 result: 7500000, time: 571 ms
WhereSelect -- mod: 4  result: 7500000, time: 501 ms
Select -- mod: 4  result 7500000, time: 406 ms
Sum -- mod: 4  result: 7500000, time: 397 ms
-------------
Where -- mod: 5 result: 7999999, time: 490 ms
WhereSelect -- mod: 5  result: 7999999, time: 477 ms
Select -- mod: 5  result 7999999, time: 397 ms
Sum -- mod: 5  result: 7999999, time: 394 ms
-------------
Where -- mod: 6 result: 9999999, time: 488 ms
WhereSelect -- mod: 6  result: 9999999, time: 480 ms
Select -- mod: 6  result 9999999, time: 391 ms
Sum -- mod: 6  result: 9999999, time: 387 ms
-------------
Where -- mod: 7 result: 8571428, time: 489 ms
WhereSelect -- mod: 7  result: 8571428, time: 486 ms
Select -- mod: 7  result 8571428, time: 384 ms
Sum -- mod: 7  result: 8571428, time: 381 ms
-------------
Where -- mod: 8 result: 8749999, time: 494 ms
WhereSelect -- mod: 8  result: 8749999, time: 488 ms
Select -- mod: 8  result 8749999, time: 386 ms
Sum -- mod: 8  result: 8749999, time: 373 ms
-------------
Where -- mod: 9 result: 9999999, time: 497 ms
WhereSelect -- mod: 9  result: 9999999, time: 494 ms
Select -- mod: 9  result 9999999, time: 386 ms
Sum -- mod: 9  result: 9999999, time: 371 ms

Для таких реалізацій:

result = source.Where(x => x.IsValid).Sum(x => x.Value);
result = source.Select(x => x.IsValid ? x.Value : 0).Sum();
result = source.Sum(x => x.IsValid ? x.Value : 0);
result = source.Where(x => x.IsValid).Select(x => x.Value).Sum();

modозначає: кожен 1 з modелементів недійсний: для mod == 1кожного товару недійсний, для mod == 2непарних елементів недійсний тощо. Колекція містить 10000000елементи.

введіть тут опис зображення

І результати для колекції з 100000000предметами:

введіть тут опис зображення

Як бачите, Selectі Sumрезультати цілком узгоджуються у всіх modзначеннях. Однак whereі where+ selectне є.


1
Дуже цікаво, що у ваших результатах усі методи починаються в одному місці та розходяться, тоді як результати ItNotALie перетинаються посередині.
Джон Ценг

6

Я здогадуюсь, що версія з Where фільтрує 0, і вони не є предметом для суми (тобто ви не виконуєте додавання). Це, звичайно, здогад, оскільки я не можу пояснити, як виконання додаткового вираження лямбда та виклик кількох методів випереджає просте додавання 0.

Мій друг припустив, що той факт, що 0 у сумі може призвести до суворого штрафу за ефективність через перевірку переповнення. Було б цікаво подивитися, як це буде працювати в неперевіреному контексті.


Деякі тести з uncheckedробить це крихітним, крихітним трохи краще для Select.
ЦеNotALie.

Чи може хтось сказати, якщо невірно встановлено вплив на методи, що викликаються стеком, або лише операції верхнього рівня?
Stilgar

1
@Stilgar Це стосується лише верхнього рівня.
Бранко Димитріевич

Тож, можливо, нам потрібно здійснити неперевірену суму і спробувати її таким чином.
Stilgar

5

Запустивши наступний зразок, мені стає зрозуміло, що єдиний раз, коли «+ Вибрати» може перевершити «Вибір» - це насправді, коли він відкидає хорошу кількість (приблизно половину в моїх неофіційних тестах) потенційних позицій у списку. У наведеному нижче невеликому прикладі я отримую приблизно однакові цифри з обох зразків, коли Де пропускає приблизно 4 мільйони предметів з 10 мільйонів. Я побіг у випуску і упорядкував виконання, де + select vs select з однаковими результатами.

static void Main(string[] args)
        {
            int total = 10000000;
            Random r = new Random();
            var list = Enumerable.Range(0, total).Select(i => r.Next(0, 5)).ToList();
            for (int i = 0; i < 4000000; i++)
                list[i] = 10;

            var sw = new Stopwatch();
            sw.Start();

            int sum = 0;

            sum = list.Where(i => i < 10).Select(i => i).Sum();            

            sw.Stop();
            Console.WriteLine(sw.ElapsedMilliseconds);

            sw.Reset();
            sw.Start();
            sum = list.Select(i => i).Sum();            

            sw.Stop();

            Console.WriteLine(sw.ElapsedMilliseconds);
        }

Може це не бути, тому що ви не відкидаєте недоліки в Select?
ЦеNotALie.

3
Запуск у налагодженні марний.
MarcinJuraszek

1
@MarcinJuraszek Очевидно. Дійсно мав на увазі сказати, що я побіг у звільненні :)
DavidN

@ ItNotALie У цьому справа. Мені здається, що єдиний спосіб, де «Select» може перевершити «Вибір», - це коли «Де» фільтрує велику кількість елементів, що підсумовуються.
DavidN

2
Це, в основному, таке моє запитання. Вони пов'язані приблизно на 60%, як це робить зразок. Питання - чому, на що тут не відповідають.
ЦеNotALie.

4

Якщо вам потрібна швидкість, просто найкраща ставка - це зробити прямий цикл. І це, forяк правило, краще, ніж foreach(якщо припустити, що ваша колекція звичайно доступна).

Ось терміни, які я отримав, коли 10% елементів недійсні:

Where + Select + Sum:   257
Select + Sum:           253
foreach:                111
for:                    61

І з 90% недійсними елементами:

Where + Select + Sum:   177
Select + Sum:           247
foreach:                105
for:                    58

І ось мій базовий код ...

public class MyClass {
    public int Value { get; set; }
    public bool IsValid { get; set; }
}

class Program {

    static void Main(string[] args) {

        const int count = 10000000;
        const int percentageInvalid = 90;

        var rnd = new Random();
        var myCollection = new List<MyClass>(count);
        for (int i = 0; i < count; ++i) {
            myCollection.Add(
                new MyClass {
                    Value = rnd.Next(0, 50),
                    IsValid = rnd.Next(0, 100) > percentageInvalid
                }
            );
        }

        var sw = new Stopwatch();
        sw.Restart();
        int result1 = myCollection.Where(mc => mc.IsValid).Select(mc => mc.Value).Sum();
        sw.Stop();
        Console.WriteLine("Where + Select + Sum:\t{0}", sw.ElapsedMilliseconds);

        sw.Restart();
        int result2 = myCollection.Select(mc => mc.IsValid ? mc.Value : 0).Sum();
        sw.Stop();
        Console.WriteLine("Select + Sum:\t\t{0}", sw.ElapsedMilliseconds);
        Debug.Assert(result1 == result2);

        sw.Restart();
        int result3 = 0;
        foreach (var mc in myCollection) {
            if (mc.IsValid)
                result3 += mc.Value;
        }
        sw.Stop();
        Console.WriteLine("foreach:\t\t{0}", sw.ElapsedMilliseconds);
        Debug.Assert(result1 == result3);

        sw.Restart();
        int result4 = 0;
        for (int i = 0; i < myCollection.Count; ++i) {
            var mc = myCollection[i];
            if (mc.IsValid)
                result4 += mc.Value;
        }
        sw.Stop();
        Console.WriteLine("for:\t\t\t{0}", sw.ElapsedMilliseconds);
        Debug.Assert(result1 == result4);

    }

}

До речі, я погоджуюсь із здогадкою Stilgar : відносна швидкість ваших двох випадків змінюється залежно від відсотка недійсних предметів, просто тому, що кількість роботи, яку Sumпотрібно виконати, варіюється у випадку "Де".


1

Замість того, щоб спробувати пояснити через опис, я буду використовувати більш математичний підхід.

З огляду на код нижче, який повинен орієнтуватися на те, що LINQ робить внутрішньо, відносні витрати такі:
Виберіть лише: Nd + Na
Де + Вибрати:Nd + Md + Ma

Щоб зрозуміти, де вони будуть перетинатися, нам потрібно зробити трохи алгебри:
Nd + Md + Ma = Nd + Na => M(d + a) = Na => (M/N) = a/(d+a)

Це означає, що для того, щоб точка перегину становила 50%, вартість виклику делегата повинна бути приблизно такою ж, як і вартість додатка. Оскільки ми знаємо, що фактична точка перегину становила близько 60%, ми можемо працювати назад і визначити, що вартість виклику делегата для @ ItNotALie насправді була приблизно 2/3 вартості додатку, що дивно, але ось що його числа говорять.

static void Main(string[] args)
{
    var set = Enumerable.Range(1, 10000000)
                        .Select(i => new MyClass {Value = i, IsValid = i%2 == 0})
                        .ToList();

    Func<MyClass, int> select = i => i.IsValid ? i.Value : 0;
    Console.WriteLine(
        Sum(                        // Cost: N additions
            Select(set, select)));  // Cost: N delegate
    // Total cost: N * (delegate + addition) = Nd + Na

    Func<MyClass, bool> where = i => i.IsValid;
    Func<MyClass, int> wSelect = i => i.Value;
    Console.WriteLine(
        Sum(                        // Cost: M additions
            Select(                 // Cost: M delegate
                Where(set, where),  // Cost: N delegate
                wSelect)));
    // Total cost: N * delegate + M * (delegate + addition) = Nd + Md + Ma
}

// Cost: N delegate calls
static IEnumerable<T> Where<T>(IEnumerable<T> set, Func<T, bool> predicate)
{
    foreach (var mc in set)
    {
        if (predicate(mc))
        {
            yield return mc;
        }
    }
}

// Cost: N delegate calls
static IEnumerable<int> Select<T>(IEnumerable<T> set, Func<T, int> selector)
{
    foreach (var mc in set)
    {
        yield return selector(mc);
    }
}

// Cost: N additions
static int Sum(IEnumerable<int> set)
{
    unchecked
    {
        var sum = 0;
        foreach (var i in set)
        {
            sum += i;
        }

        return sum;
    }
}

0

Я думаю, що цікаво, що результат MarcinJuraszek відрізняється від результатів ItNotALie. Зокрема, результати MarcinJuraszek починаються з усіх чотирьох реалізацій на одному місці, тоді як результати ItNotALie перетинаються посередині. Я поясню, як це працює з джерела.

Припустимо, що існують nзагальні елементи та mдійсні елементи.

SumФункція досить проста. Він просто проходить через перелік: http://typedescriptor.net/browse/members/367300-System.Linq.Enumerable.Sum(IEnumerable%601)

Для простоти припустимо, що колекція - це список. І Select, і SelectSelect створять a WhereSelectListIterator. Це означає, що фактично створені ітератори однакові. В обох випадках є Sumщо перебирає ітератора, то WhereSelectListIterator. Найцікавіша частина ітератора - MoveNext метод .

Оскільки ітератори однакові, петлі однакові. Єдина відмінність - в тілі петель.

Тіло цих лямбдів має дуже схожу вартість. Клауза де повертає значення поля, а потрійний предикат також повертає значення поля. Стаття вибору повертає значення поля, а дві гілки потрійного оператора повертають або значення поля, або константу. У комбінованому пункті вибору є гілка як оператор потрійної, але WhereSelect використовує гілку в MoveNext.

Однак усі ці операції є досить дешевими. Найдорожча операція поки що - це філія, де нам буде коштувати неправильний прогноз.

Ще одна дорога операція тут Invoke. Викликання функції займає трохи більше часу, ніж додавання значення, як показав Бранко Димитрієвич.

Також зважування - це перевірене скупчення в Sum . Якщо процесор не має арифметичного прапора переповнення, перевірити це також можна дорого.

Отже, цікаві витрати:

  1. ( n+ m) * Викликати + m*checked+=
  2. n* Викликати + n*checked+=

Таким чином, якщо вартість Invoke набагато вище, ніж вартість перевіреного накопичення, то випадок 2 завжди кращий. Якщо вони приблизно рівні, тоді ми побачимо баланс, коли приблизно половина елементів є дійсними.

Схоже, що в системі MarcinJuraszek, перевірено + = має незначну вартість, але в системах ItNotALie та Бранко Димитрієвич перевірено + = має значні витрати. Схоже, це найдорожче в системі ItNotALie, оскільки точка беззбитковості набагато вище. Схоже, ніхто не опублікував результати системи, де накопичення коштує набагато дорожче, ніж Invoke.


@ ItNotALie. Я не думаю, що хтось має неправильний результат. Я просто не міг пояснити деякі речі. Я припускав, що вартість Invoke набагато вище, ніж + =, але можливо, що вони можуть бути набагато ближче залежно від апаратних оптимізацій.
Джон Ценг
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.