C # проти C - велика різниця в продуктивності


94

Я знаходжу значні відмінності в продуктивності між подібним кодом у C anc C #.

Код С:

#include <stdio.h>
#include <time.h>
#include <math.h>

main()
{
    int i;
    double root;

    clock_t start = clock();
    for (i = 0 ; i <= 100000000; i++){
        root = sqrt(i);
    }
    printf("Time elapsed: %f\n", ((double)clock() - start) / CLOCKS_PER_SEC);   

}

А C # (консольний додаток):

using System;
using System.Collections.Generic;
using System.Text;

namespace ConsoleApplication2
{
    class Program
    {
        static void Main(string[] args)
        {
            DateTime startTime = DateTime.Now;
            double root;
            for (int i = 0; i <= 100000000; i++)
            {
                root = Math.Sqrt(i);
            }
            TimeSpan runTime = DateTime.Now - startTime;
            Console.WriteLine("Time elapsed: " + Convert.ToString(runTime.TotalMilliseconds/1000));
        }
    }
}

З наведеним вище кодом C # завершується за 0,328125 секунд (версія випуску), а C займає 11,14 секунди.

С компілюється до виконуваного файлу Windows за допомогою mingw.

Я завжди припускав, що C / C ++ були швидшими або принаймні порівнянними з C # .net. Що саме призводить до того, що C працює над 30 разів повільніше?

РЕДАГУВАТИ: Здається, оптимізатор C # видаляв корінь, оскільки він не використовувався. Я змінив кореневе призначення на root + = і роздрукував загальну суму в кінці. Я також скомпілював C, використовуючи cl.exe з прапором / O2, встановленим для максимальної швидкості.

Результати: 3,75 секунди для C 2,61 секунди для C #

C все ще займає більше часу, але це прийнятно


18
Я б запропонував вам використовувати секундомір замість просто DateTime.
Алекс Форт

2
Прапорці якого компілятора? Чи ввімкнуто обидва варіанти з оптимізацією?
jalf

2
А що, коли ви використовуєте -ffast-math із компілятором C ++?
Dan McClain

10
Яке захоплююче питання!
Роберт С.

4
Можливо, функція C sqrt не така добра, як у C #. Тоді це буде не проблема C, а бібліотека, приєднана до неї. Спробуйте деякі обчислення без математичних функцій.
klew

Відповіді:


61

Оскільки ви ніколи не використовуєте 'root', компілятор, можливо, видаляв виклик для оптимізації вашого методу.

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

Редагувати: див . Відповідь Джалфа нижче


1
Трохи експериментів свідчать, що це не так. Код циклу генерується, хоча, можливо, час виконання досить розумний, щоб його пропустити. Навіть накопичуючись, C # все ще б'є штани C.
Дана

3
Здається, проблема в іншому кінці. C # поводиться розумно у всіх випадках. Його код С, очевидно, складений без оптимізації
jalf

2
Багато з вас тут втрачають суть. Я читав багато подібних випадків, коли c # перевершує c / c ++, і завжди спростування полягає у використанні певної оптимізації рівня експертів. 99% програмістів не мають знань використовувати такі методи оптимізації лише для того, щоб їх код працював трохи швидше, ніж код c #. Варіанти використання для c / c ++ звужуються.

167

Ви повинні порівнювати збірки налагодження. Я щойно скомпілював ваш C-код і отримав

Time elapsed: 0.000000

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

Здається, те, що ви вимірюєте, в основному полягає в тому, "який компілятор вставляє найбільші накладні витрати". І виявляється, відповідь C. Але це не говорить нам, яка програма найшвидша. Тому що коли вам потрібна швидкість, ви вмикаєте оптимізацію.

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

У мові С є певні речі, які були б ефективними навіть у наївному не оптимізуючому компіляторі, а є й інші, які в значній мірі покладаються на компілятор, щоб оптимізувати все. І звичайно, те саме стосується C # або будь-якої іншої мови.

Швидкість виконання визначається:

  • платформа, на якій ви працюєте (ОС, апаратне забезпечення, інше програмне забезпечення, що працює в системі)
  • компілятор
  • ваш вихідний код

Хороший компілятор C # дасть ефективний код. Поганий компілятор C генерує повільний код. А як щодо компілятора C, який створив код C #, який потім можна було б запустити через компілятор C #? Як швидко це бігло? Мови не мають швидкості. Ваш код робить.


Ще багато цікавого читання тут: blogs.msdn.com/ricom/archive/2005/05/10/416151.aspx
Даніель Ервікер

18
Хороша відповідь, але я не погоджуюся з мовною швидкістю, принаймні за аналогією: Встановлено, що валлійська мова є повільнішою за більшість із-за високої частоти довгих голосних. Крім того, люди краще запам'ятовують слова (і списки слів), якщо вони швидше вимовляють. web.missouri.edu/~cowann/docs/articles/before%201993/… en.wikipedia.org/wiki/Vowel_length en.wikipedia.org/wiki/Welsh_language
exceptionerror

1
Хіба це не залежить від того, що ви говорите по-вельш? Я вважаю навряд чи все повільніше.
jalf

5
++ Гей, хлопці, не заходьте сюди. Якщо одна і та ж програма працює швидше однією мовою, ніж інша, це тому, що генерується інший код збірки. У цьому конкретному прикладі 99% або більше часу піде на плаваюче i, і sqrt, отже, це те, що вимірюється.
Mike Dunlavey

116

Я буду коротко писати, це вже позначено як відповідь. C # має велику перевагу в тому, що має чітко визначену модель з плаваючою комою. Це якраз відповідає рідному режиму роботи набору інструкцій FPU та SSE на процесорах x86 та x64. Немає випадковості там. JITter компілює Math.Sqrt () за кількома вбудованими інструкціями.

Рідний C / C ++ обтяжений роками зворотної сумісності. Параметри / fp: точний, / fp: швидкий та / fp: строгі компіляції є найбільш видимими. Відповідно, він повинен викликати функцію ЕПТ, яка реалізує sqrt () і перевіряє вибрані параметри з плаваючою комою для коригування результату. Це повільно.


66
Це дивне переконання серед програмістів на C ++, здається, вони думають, що машинний код, сформований C #, якимось чином відрізняється від машинного коду, сформованого власним компілятором. Є лише один вид. Незалежно від того, який перемикач компілятора gcc ви використовуєте або вбудовану збірку, яку ви пишете, все одно існує лише одна інструкція FSQRT. Це не завжди швидше, тому що його створила рідна мова, процесору все одно.
Ганс Пасант,

16
Ось що вирішує попереднє джитування за допомогою ngen.exe. Ми говоримо про C #, а не про Java.
Ганс Пасант,

20
@ user877329 - справді? Ого.
Андраш Золтан,

7
Ні, джиттер x64 використовує SSE. Math.Sqrt () переводиться в інструкцію машинного коду sqrtsd.
Ганс Пасант,

6
Хоча технічно це не різниця між мовами, .net JITter робить досить обмежені оптимізації порівняно із типовим компілятором C / C ++. Одним з найбільших обмежень є відсутність підтримки SIMD, що робить код часто приблизно в чотири рази повільнішим. Не викриття багатьох властивостей також може бути великим малусом, але це багато в чому залежить від того, що ви робите.
CodesInChaos

57

Я C ++ і розробник C #. Я розробляв програми C # з першої бета-версії .NET Framework і мав більше 20 років досвіду у розробці програм C ++. По-перше, код С # НІКОЛИ не буде швидшим, ніж додаток С ++, але я не буду довго обговорювати керований код, як він працює, рівень взаємодії, внутрішні елементи управління пам'яттю, систему динамічного типу та збирач сміття. Тим не менше, дозвольте продовжити, сказавши, що перелічені тут контрольні показники дають НЕПРАВИЛЬНІ результати.

Поясню: перше, що нам потрібно врахувати, це компілятор JIT для C # (.NET Framework 4). Тепер JIT виробляє власний код для центрального процесора, використовуючи різні алгоритми оптимізації (які, як правило, більш агресивні, ніж оптимізатор за замовчуванням C ++, який постачається з Visual Studio), і набір команд, що використовується компілятором .NET JIT, є більш точним відображенням фактичного процесора на машині, щоб можна було зробити певні заміни в машинному коді, щоб зменшити тактові цикли та покращити частоту потрапляння в кеш конвеєра процесора та провести подальші оптимізації гіперпотоків, такі як переупорядкування інструкцій та вдосконалення, пов'язані з прогнозуванням гілок.

Це означає, що якщо ви не скомпілюєте свою програму C ++, використовуючи правильні параметри для збірки RELEASE (а не збірки DEBUG), тоді ваша програма C ++ може працювати повільніше, ніж відповідна програма на базі C # або .NET. Вказуючи властивості проекту у вашому додатку C ++, переконайтеся, що ви ввімкнули "повну оптимізацію" та "віддайте перевагу швидкому коду". Якщо у вас 64-розрядна машина, ви ПОВИННІ вказати для генерації x64 як цільову платформу, інакше ваш код буде виконаний через підшар перетворення (WOW64), що істотно знизить продуктивність.

Після того, як ви виконаєте правильну оптимізацію в компіляторі, я отримую 0,72 секунди для програми C ++ та 1,16 секунди для програми C # (обидві у збірці випуску). Оскільки додаток C # є дуже простим і виділяє пам'ять, що використовується в циклі, у стеці, а не в купі, насправді він працює набагато краще, ніж реальний додаток, що бере участь у об'єктах, важких обчисленнях і з більшими наборами даних. Отже, наведені цифри - це оптимістичні цифри, упереджені до C # та .NET framework. Навіть маючи таке упередження, програма C ++ виконується трохи більше половини часу, ніж еквівалентна програма C #. Майте на увазі, що компілятор Microsoft C ++, який я використовував, не мав правильної оптимізації конвеєра та гіперпотоків (за допомогою WinDBG для перегляду інструкцій з монтажу).

Тепер, якщо ми використовуємо компілятор Intel (який, до речі, є галузевим секретом для створення високопродуктивних додатків на процесорах AMD / Intel), той самий код виконується за .54 секунди для виконуваного файлу C ++ проти .72 секунди за допомогою Microsoft Visual Studio 2010 Тож, врешті-решт, кінцеві результати становлять 0,5 секунди для С ++ та 1,16 секунди для С #. Отже, створення коду компілятором .NET JIT займає в 214% разів більше часу, ніж виконуваний файл C ++. Велику частину часу, проведеного за .54 секунди, отримували час від системи, а не в самому циклі!

У статистиці також бракує часу запуску та очищення, які не включені в терміни. Програми C # зазвичай витрачають на запуск та припинення набагато більше часу, ніж програми C ++. Причина цього складна і пов’язана з процедурами перевірки коду виконання .NET та підсистемою управління пам’яттю, яка виконує багато робіт на початку (а отже, і в кінці) програми з оптимізації розподілу пам’яті та сміття колектор.

Вимірюючи продуктивність C ++ та .NET IL, важливо поглянути на код збірки, щоб переконатися, що ВСІ розрахунки є. Я виявив, що без введення додаткового коду в C # більшість коду у прикладах вище фактично були видалені з двійкового файлу. Це було також у випадку з C ++, коли ви використовували більш агресивний оптимізатор, такий як той, що постачається з компілятором Intel C ++. Результати, які я надав вище, на 100% правильні та підтверджені на рівні складання.

Основна проблема багатьох форумів в Інтернеті полягає в тому, що багато новачків слухають маркетингову пропаганду Microsoft, не розуміючи технології, і роблять неправдиві твердження, що C # швидший за C ++. Твердження полягає в тому, що теоретично C # працює швидше, ніж C ++, оскільки компілятор JIT може оптимізувати код для процесора. Проблема цієї теорії полягає в тому, що в середовищі .NET існує багато сантехніки, яка сповільнює продуктивність; сантехніка, якої не існує в додатку C ++. Крім того, досвідчений розробник знатиме правильний компілятор, який використовуватиметься для даної платформи, і використовувати відповідні прапори під час компіляції програми. На платформах Linux або з відкритим кодом це не проблема, оскільки ви можете розповсюджувати своє джерело та створювати сценарії встановлення, які компілюють код, використовуючи відповідну оптимізацію. На вікнах або платформі із закритим кодом вам доведеться поширювати кілька виконуваних файлів, кожен із яких має певні оптимізації. Бінарні файли Windows, які будуть розгорнуті, базуються на центральному процесорі, виявленому програмою встановлення msi (за допомогою спеціальних дій).


22
1. Корпорація Майкрософт ніколи не заявляла про те, що C # є швидшим, оскільки їхні вимоги складають приблизно 90% швидкості, швидше розробляються (а отже, і більше часу на налаштування) та більше не містять помилок завдяки безпеці пам’яті та типу. Усі вони відповідають дійсності (у мене 20 років на C ++ і 10 на C #) 2. Ефективність запуску в більшості випадків безглузда. 3. Є також більш швидкі компілятори C #, такі як LLVM (тому виведення Intel - це не яблука до яблук)
бен

13
Ефективність запуску не має сенсу. Це дуже важливо для більшості корпоративних веб-додатків, тому Microsoft представила веб-сторінки, які попередньо завантажуються (автозапуск) у .NET 4.0. Коли пул програм рециркулюється раз у раз, перше завантаження кожної сторінки додасть значну затримку для складних сторінок і спричинить тайм-аути в браузері.
Річард

8
Корпорація Майкрософт заявила про швидкість роботи .NET у попередніх маркетингових матеріалах. Вони також висловлювали різні претензії щодо збирача сміття, мало чи майже не впливали на ефективність роботи. Деякі з цих тверджень потрапили до різних книг (про ASP.NET та .NET) у своїх попередніх виданнях. Незважаючи на те, що Microsoft конкретно не говорить, що ваша програма C # буде швидшою, ніж ваша програма C ++, вони можуть містити загальні коментарі та маркетингові слогани, такі як " Вчасно встигнути швидко" ( msdn.microsoft.com/ en-us / library / ms973894.aspx ).
Річард

71
-1, цей виступ сповнений некоректних та оманливих тверджень, таких як очевидний колокольчик "Код C # НІКОЛИ не буде швидшим, ніж додаток С ++"
BCoates

32
-1. Вам слід прочитати Ріко Маріані проти битви за ефективність C # проти C Реймонда Чена: blogs.msdn.com/b/ricom/archive/2005/05/16/418051.aspx . Коротше кажучи: одному з найрозумніших хлопців у Microsoft знадобилося багато оптимізації, щоб зробити версію C швидшою, ніж просту версію C #.
Rolf Bjarne Kvinge

10

моє перше здогадування - це оптимізація компілятора, оскільки ви ніколи не використовуєте root. Ви просто призначаєте його, а потім перезаписуєте знову і знову.

Редагувати: блін, бити на 9 секунд!


2
Я кажу, що ви праві. Фактична змінна перезаписується і більше ніколи не використовується. CSC, швидше за все, просто відмовиться від цілого циклу, тоді як компілятор c ++, мабуть, залишив його. Більш точним тестом було б накопичення результатів, а потім друк результату в кінці. Також не слід жорстко кодувати значення насіння, а краще залишати його визначеним користувачем. Це не дасть компілятору c # можливості залишити речі поза увагою.

7

Щоб побачити, чи оптимізовано цикл, спробуйте змінити код на

root += Math.Sqrt(i);

ans аналогічно в коді C, а потім надрукуйте значення root поза циклом.


6

Можливо, компілятор c # помічає, що ви ніде не використовуєте root, тому він просто пропускає весь цикл for. :)

Це може бути не так, але я підозрюю, що б не була причина, це залежить від реалізації компілятора. Спробуйте скомпілювати програму C із компілятором Microsoft (cl.exe, доступний як частина win32 sdk) з оптимізацією та режимом випуску. Б'юся об заклад, ви побачите покращення в порівнянні з іншим компілятором.

EDIT: Я не думаю, що компілятор може просто оптимізувати цикл for, оскільки він повинен знати, що Math.Sqrt () не має побічних ефектів.


2
Можливо, воно це знає.

2
@Neil, @jeff: Згоден, він міг це знати досить легко. Залежно від реалізації, статичний аналіз на Math.Sqrt () може бути не таким важким, хоча я не впевнений, які оптимізації конкретно виконуються.
Джон Фемінелла

5

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

Можливо, вам слід спробувати виграти. еквівалентно $ / usr / bin / time my_cprog; / usr / bin / time my_csprog


1
Чому це проти? Хтось припускає, що переривання та перемикання контексту не впливають на продуктивність? Хтось може робити припущення щодо пропусків TLB, обміну сторінками тощо?
Том

5

Я склав (на основі вашого коду) ще два порівнянні тести на C та C #. Ці двоє записують менший масив, використовуючи модуль-оператор для індексації (це додає трохи накладних витрат, але привіт, ми намагаємось порівняти продуктивність [на сирому рівні]).

Код C:

#include <stdlib.h>
#include <stdio.h>
#include <time.h>
#include <math.h>

void main()
{
    int count = (int)1e8;
    int subcount = 1000;
    double* roots = (double*)malloc(sizeof(double) * subcount);
    clock_t start = clock();
    for (int i = 0 ; i < count; i++)
    {
        roots[i % subcount] = sqrt((double)i);
    }
    clock_t end = clock();
    double length = ((double)end - start) / CLOCKS_PER_SEC;
    printf("Time elapsed: %f\n", length);
}

У C #:

using System;

namespace CsPerfTest
{
    class Program
    {
        static void Main(string[] args)
        {
            int count = (int)1e8;
            int subcount = 1000;
            double[] roots = new double[subcount];
            DateTime startTime = DateTime.Now;
            for (int i = 0; i < count; i++)
            {
                roots[i % subcount] = Math.Sqrt(i);
            }
            TimeSpan runTime = DateTime.Now - startTime;
            Console.WriteLine("Time elapsed: " + Convert.ToString(runTime.TotalMilliseconds / 1000));
        }
    }
}

Ці тести записують дані в масив (тому середовищі виконання .NET не слід дозволяти відбирати sqrt op), хоча масив значно менший (не хотів використовувати надмірну пам'ять). Я скомпілював їх у конфігурації випуску та запустив зсередини вікна консолі (замість запуску через VS).

На моєму комп'ютері програма C # варіюється від 6,2 до 6,9 секунди, тоді як версія C варіюється від 6,9 до 7,1.


5

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

Не потрібно освічених здогадок.


Я хотів би знати, як це зробити
Джош Стодола

Залежить від вашої IDE або налагоджувача. Перерва на початку пгм. Відобразіть вікно демонтажу та починайте покроково. Якщо ви використовуєте GDB, існують команди для покрокової вказівки за раз.
Mike Dunlavey

Тепер це хороша порада, це допомагає набагато більше зрозуміти, що насправді відбувається там, внизу. Чи показує це також оптимізацію JIT, як вбудовування та зворотні дзвінки?
gjvdkamp

FYI: для мене це показало VC ++ за допомогою fadd та fsqrt, тоді як C # використовував cvtsi2sd та sqrtsd, які, наскільки я розумію, є інструкціями SSE2, і набагато швидше там, де вони підтримуються.
danio

2

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


Теоретично так. На практиці це практично ніколи не робить помітної різниці. Можливо, відсоток-два, якщо вам пощастить.
jalf

або - якщо у вас є певний тип коду, який використовує розширення, яких немає в списку дозволених для `` загального '' процесора. Такі речі, як аромати SSE. Спробуйте встановити вищу ціль процесора, щоб побачити, які відмінності ви отримуєте.
gbjbaanb

1

Мені здається, це не має нічого спільного з самими мовами, скоріше це стосується різних реалізацій функції квадратного кореня.


Я дуже сумніваюся, що різні реалізації sqrt можуть спричинити таку різницю.
Алекс Форт

Тим більше, що навіть у C # більшість математичних функцій все ще вважаються критично важливими для продуктивності і реалізуються як такі.
Matthew Olenik

fsqrt - це інструкція процесора IA-32, тому реалізація мови в наш час неактуальна.
Не впевнений

Крок у функцію sqrt MSVC за допомогою налагоджувача. Це робить набагато більше, ніж просто виконання інструкції fsqrt.
bk1e

1

Насправді, хлопці, цикл НЕ оптимізується. Я склав код Джона і вивчив отриманий файл .exe. Кишки петлі такі:

 IL_0005:  stloc.0
 IL_0006:  ldc.i4.0
 IL_0007:  stloc.1
 IL_0008:  br.s       IL_0016
 IL_000a:  ldloc.1
 IL_000b:  conv.r8
 IL_000c:  call       float64 [mscorlib]System.Math::Sqrt(float64)
 IL_0011:  pop
 IL_0012:  ldloc.1
 IL_0013:  ldc.i4.1
 IL_0014:  add
 IL_0015:  stloc.1
 IL_0016:  ldloc.1
 IL_0017:  ldc.i4     0x5f5e100
 IL_001c:  ble.s      IL_000a

Хіба що час виконання розумний, щоб зрозуміти, що цикл нічого не робить і пропускає його?

Редагувати: Зміна C # на:

 static void Main(string[] args)
 {
      DateTime startTime = DateTime.Now;
      double root = 0.0;
      for (int i = 0; i <= 100000000; i++)
      {
           root += Math.Sqrt(i);
      }
      System.Console.WriteLine(root);
      TimeSpan runTime = DateTime.Now - startTime;
      Console.WriteLine("Time elapsed: " +
          Convert.ToString(runTime.TotalMilliseconds / 1000));
 }

Результати за час, що минув (на моїй машині) переходив від 0,047 до 2,17. Але чи це лише накладні витрати на додавання 100 мільйонів операторів додавання?


3
Погляд на IL не говорить багато про оптимізацію, тому що, хоча компілятор C # робить деякі речі, такі як постійне згортання та видалення мертвого коду, IL потім бере на себе та виконує решту під час завантаження.
Даніель Ервікер

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