Хороший приклад масиву змінної довжини C [закритий]


9

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

Чи можете ви навести приклад , коли використання C99 VLA надає реальну перевагу перед чимось на зразок поточних стандартних купових механізмів, що використовують C ++ RAII механізми?

Приклад, за яким я після:

  1. Досягніть легко вимірюваної (на 10% можливо) переваги в порівнянні з використанням купи.
  2. Не мати хорошого вирішення, яке взагалі не знадобилося б для всього масиву.
  3. Фактично виграш від використання динамічного розміру замість фіксованого максимального розміру.
  4. У звичайному сценарії використання навряд чи викличете переповнення стека.
  5. Будьте достатньо сильні, щоб спокусити розробника, який потребує продуктивності, щоб включити вихідний файл C99 у проект C ++.

Додавання деяких пояснень у контексті: я маю на увазі VLA як мається на увазі під C99 і не включений у стандарт C ++: int array[n]де nє змінною. Я переглядаю приклад використання, коли він перетворює альтернативи, запропоновані іншими стандартами (C90, C ++ 11):

int array[MAXSIZE]; // C stack array with compile time constant size
int *array = calloc(n, sizeof int); // C heap array with manual free
int *array = new int[n]; // C++ heap array with manual delete
std::unique_ptr<int[]> array(new int[n]); // C++ heap array with RAII
std::vector<int> array(n); // STL container with preallocated size

Деякі ідеї:

  • Функції, що приймають вараги, що, природно, обмежує кількість елементів до чогось розумного, але не має жодної корисної верхньої межі рівня API.
  • Рекурсивні функції, де витрачається стек небажано
  • Багато невеликих асигнувань і випусків, де купа грошових витрат буде поганою.
  • Обробка багатовимірних масивів (на зразок матриць довільного розміру), де продуктивність є критичною, а малі функції, як очікується, будуть багато вбудовані.
  • З коментаря: паралельний алгоритм, де розподіл купи має накладні синхронізації .

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

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


1
Трохи умоглядний (оскільки це молоток, який шукає цвях), але, можливо alloca(), справді затьмарить malloc()у багатопотоковому середовищі через суперечки замка в останньому . Але це справжня розтягнення, оскільки малі масиви повинні просто використовувати фіксований розмір, а великим масивам, напевно, потрібна буде купа.
chrisaycock

1
@chrisaycock Так, дуже багато молотка шукає цвяха, але молот, який насправді існує (будь то C99 VLA чи не фактично-в-будь-якому стандарті alloca, що, на мою думку, в основному одне і те ж). Але ця багатопотокова річ хороша, редагуючи питання, щоб включити її!
Гайда

Одним з недоліків VLAs є те, що не існує механізму виявлення відмови у виділенні; якщо недостатньо пам'яті, поведінка не визначена. (Те саме стосується масивів фіксованого розміру - і для alloca ().)
Кіт Томпсон,

@KeithThompson Ну, немає жодної гарантії, що malloc / new також виявить помилку розподілу, наприклад, див. Сторінку Примітки для Linux manloc man ( linux.die.net/man/3/malloc ).
Гайда

@hyde: І дискусійно, чи mallocвідповідає поведінка Linux стандарту C.
Кіт Томпсон

Відповіді:


9

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

Для ДУЖЕ малих масивів це показує явну вигоду від закінчення VLA std::vector<>.

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

Для ДУЖЕ малих значень "кількості випадкових чисел" (x) у відповідних функціях vlaрішення виграє з величезним відривом. Оскільки розмір збільшується, "виграш" стає меншим, і, враховуючи достатній розмір, векторне рішення виявляється БІЛЬШЕ ефективним - не вивчав цей варіант занадто багато, як коли ми починаємо мати тисячі елементів у VLA, це не дійсно те, що вони мали на меті зробити ...

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

Запускаючи цей конкретний варіант, я отримую приблизно 10% різниці між func1(VLA) та func2(std :: vector).

count = 9884
func1 time in clocks per iteration 7048685
count = 9884
func2 time in clocks per iteration 7661067
count = 9884
func3 time in clocks per iteration 8971878

Це складено з: g++ -O3 -Wall -Wextra -std=gnu++0x -o vla vla.cpp

Ось код:

#include <iostream>
#include <vector>
#include <cstdint>
#include <cstdlib>

using namespace std;

const int SIZE = 1000000;

uint64_t g_val[SIZE];


static __inline__ unsigned long long rdtsc(void)
{
    unsigned hi, lo;
    __asm__ __volatile__ ("rdtsc" : "=a"(lo), "=d"(hi));
    return ( (unsigned long long)lo)|( ((unsigned long long)hi)<<32 );
}


int func1(int x)
{
    int v[x];

    int vmax = 0;
    int vmin = x;
    for(int i = 0; i < x; i++)
    {
        v[i] = rand() % x;
        if (v[i] > vmax) 
            vmax = v[i];
        if (v[i] < vmin) 
            vmin = v[i];
    }
    int avg = (vmax + vmin) / 2;
    int count = 0;
    for(int i = 0; i < x; i++)
    {
        if (v[i] > avg)
        {
            count++;
        }
    }
    return count;
}

int func2(int x)
{
    vector<int> v;
    v.resize(x); 

    int vmax = 0;
    int vmin = x;
    for(int i = 0; i < x; i++)
    {
        v[i] = rand() % x;
        if (v[i] > vmax) 
            vmax = v[i];
        if (v[i] < vmin) 
            vmin = v[i];
    }
    int avg = (vmax + vmin) / 2;
    int count = 0;
    for(int i = 0; i < x; i++)
    {
        if (v[i] > avg)
        {
            count++;
        }
    }
    return count;
}    

int func3(int x)
{
    vector<int> v;

    int vmax = 0;
    int vmin = x;
    for(int i = 0; i < x; i++)
    {
        v.push_back(rand() % x);
        if (v[i] > vmax) 
            vmax = v[i];
        if (v[i] < vmin) 
            vmin = v[i];
    }
    int avg = (vmax + vmin) / 2;
    int count = 0;
    for(int i = 0; i < x; i++)
    {
        if (v[i] > avg)
        {
            count++;
        }
    }
    return count;
}    

void runbench(int (*f)(int), const char *name)
{
    srand(41711211);
    uint64_t long t = rdtsc();
    int count = 0;
    for(int i = 20; i < 200; i++)
    {
        count += f(i);
    }
    t = rdtsc() - t;
    cout << "count = " << count << endl;
    cout << name << " time in clocks per iteration " << dec << t << endl;
}

struct function
{
    int (*func)(int);
    const char *name;
};


#define FUNC(f) { f, #f }

function funcs[] = 
{
    FUNC(func1),
    FUNC(func2),
    FUNC(func3),
}; 


int main()
{
    for(size_t i = 0; i < sizeof(funcs)/sizeof(funcs[0]); i++)
    {
        runbench(funcs[i].func, funcs[i].name);
    }
}

Ого, моя система демонструє 30% -ве покращення версії VLA std::vector.
chrisaycock

1
Ну, спробуйте з діапазоном розмірів приблизно 5-15 замість 20-200, і ви, мабуть, матимете поліпшення 1000% або більше. [Також залежить від параметрів компілятора - я відредагую вищевказаний код, щоб показати свої параметри компілятора на gcc]
Mats Petersson

Я щойно додав, func3який використовує v.push_back(rand())замість цього v[i] = rand();і усуває потребу в resize(). Це займає приблизно 10% більше в порівнянні з використовуваним resize(). [Звичайно, під час цього процесу я виявив, що використання v[i]головного внеску в той час, як функція займає - я трохи здивований цьому].
Матс Петерсон

1
@MikeBrown Чи знаєте ви про реальну std::vectorреалізацію, яка б використовувала VLA / alloca, або це лише спекуляція?
Гайда

3
Вектор дійсно використовує масив внутрішньо, але, наскільки я розумію, він не має можливості використовувати VLA. Я вважаю, що мій приклад показує, що VLA корисні в деяких (можливо, навіть багатьох) випадках, коли обсяг даних невеликий. Навіть якщо вектор використовує VLA, це було б після додаткових зусиль всередині vectorреалізації.
Матс Петерсон

0

Щодо VLAs проти вектора

Чи вважали ви, що вектор може скористатися самими VLAs. Без VLAs вектор має вказати певні "масштаби" масивів, наприклад, 10, 100, 10000 для зберігання, щоб у кінцевому підсумку виділити масив 10000 елементів для вміщення 101 елемента. Якщо VLA, якщо ви зміните розмір до 200, алгоритм може припустити, що вам знадобиться лише 200 і можете виділити масив 200 елементів. Або він може виділити буфер сказати n * 1,5.

У всякому разі, я б заперечував, що якщо ви знаєте, скільки предметів вам знадобиться під час виконання, VLA буде більш ефективним (як показав показник Матса). Він продемонстрував просту ітерацію з двома проходами. Подумайте про симуляції Монте-Карло, де випадкові зразки беруться неодноразово, або маніпулювання зображеннями (як фільтри Photoshop), де обчислення проводяться на кожному елементі кілька разів, і, можливо, кожне обчислення кожного елемента включає перегляд сусідів.

Цей додатковий стрибок вказівника з вектора на його внутрішній масив додається.

Відповідаючи на головне питання

Але коли ви говорите про використання динамічно розподіленої структури, як LinkedList, порівняння немає. Масив забезпечує прямий доступ, використовуючи арифметику вказівника до його елементів. Використовуючи пов'язаний список, ви повинні пройтися по вузлах, щоб дістатися до певного елемента. Тож VLA виграє руки в цьому сценарії.

Відповідно до цієї відповіді , це залежить від архітектури, але в деяких випадках доступ до пам’яті до стека буде швидшим за рахунок наявності стеку в кеші. З великою кількістю елементів це може бути заперечене (потенційно причиною зменшення повернення Матс бачив його орієнтири). Однак варто зазначити, що розміри кешу істотно зростають, і ви, можливо, побачите більше, що число зростає відповідно.


Я не впевнений, що розумію вашу посилання на пов'язані списки, тому я додав розділ до питання, дещо більше пояснюючи контекст і додаючи приклади альтернатив, про які я думаю.
Гайда

Навіщо std::vectorпотрібні шкали масивів? Навіщо йому потрібно місця для 10K елементів, коли йому потрібно лише 101? Крім того, питання ніколи не згадує пов'язані списки, тому я не впевнений, звідки ви це взяли. Нарешті, VLA в C99 виділяються стеком; вони є стандартною формою alloca(). Все, що вимагає зберігання у купі (воно живе навколо після повернення функції) або a realloc()(масив змінює розмір), все одно забороняє VLA.
chrisaycock

@chrisaycock чомусь не вистачає функції realloc (), якщо пам'ять виділяється за допомогою нового []. Хіба це не головна причина, чому std :: vector повинен використовувати ваги?

@Lundin Чи С ++ масштабує вектор потужністю десять? У мене просто склалося враження, що Майк Браун був справді розгублений питанням, враховуючи посилання на пов'язаний список. (Він також висловив попереднє твердження, що мав на увазі, що V99 VLA живуть на купі.)
chrisaycock

@hyde Я не здогадувався, що про це ти говориш. Я думав, ти маєш на увазі інші структури даних на основі купи. Цікаво зараз, коли ви додали це пояснення. Мені недостатньо видовища С ++, щоб сказати вам різницю між ними.
Майкл Браун

0

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

Насправді, у більшості місць, де VLA використовуються таким чином, вони не замінюють купі виклики, а замінюють щось на кшталт:

float vals[256]; /* I hope we never get more! */

Річ у будь-якій місцевій декларації полягає в тому, що це надзвичайно швидко. Лінія float vals[n]зазвичай вимагає лише декількох інструкцій процесора (можливо, лише одну.) Вона просто додає значення в nпокажчик стека.

З іншого боку, для розподілу купи потрібна структура даних для пошуку вільної області. Час, ймовірно, на порядок довший навіть у найщасливішому випадку. (Тобто, лише акт розміщення nна стеку та виклик malloc- це, мабуть, 5-10 інструкцій.) Можливо, набагато гірше, якщо в купі є якийсь розумний обсяг даних. Мене зовсім би не здивувало, коли я бачив випадок, коли mallocв реальній програмі було 100х до 1000х повільніше.

Звичайно, тоді ви також матимете деякий вплив на ефективність із збігом free, ймовірно, за величиною схожим на mallocвиклик.

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


Про приклад Вікіпедії: це може бути частиною хорошого прикладу, але без контексту, більше коду навколо нього, він насправді не показує жодної з 5 речей, перелічених у моєму питанні. Інакше так, я згоден з вашим поясненням. Хоча потрібно пам’ятати про одне: використання VLA може мати витрати на доступ до локальних змінних, але зсуви всіх локальних змінних не обов’язково відомі під час компіляції, тому потрібно бути обережним, щоб не замінювати разові витрати на грошові внутрішнє циклічне покарання за кожну ітерацію.
Гайда

Гм ... не впевнений, що ти маєш на увазі. Декларації локальної змінної - це одна операція, і будь-який м'яко оптимізований компілятор виведе розподіл із внутрішнього циклу. Немає особливих "витрат" на доступ до локальних змінних, звичайно, не така, яка збільшуватиме VLA.
Gort the Robot

Конкретний приклад: int vla[n]; if(test()) { struct LargeStruct s; int i; }зміщення стека sне буде відомим під час компіляції, а також сумнівно, чи компілятор перемістить сховище із iвнутрішньої області на зміщення фіксованого стека. Тож потрібен додатковий машинний код, оскільки непряме, і це також може з'їсти регістри, важливі на апаратному забезпеченні ПК. Якщо ви хочете прикладу коду з включеним результатом збірки компілятора, будь ласка, задайте окреме запитання;)
hyde

Компілятору не потрібно виділяти порядок, що зустрічається в коді, і не має значення, виділяється чи не використовується простір. Розумний оптимізатор виділить простір для sта iпри введенні функції, перш ніж testвикликається або vlaрозподіляється, як виділення для побічних ефектів sі iне мають. (І насправді iможе бути навіть розміщено в реєстрі, це означає, що взагалі немає "виділення".) Немає гарантій компілятора на порядок виділень на стеку або навіть на те, що стек використовується.
Gort the Robot

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