Чи краще використовувати std :: memcpy () або std :: copy () з точки зору продуктивності?


163

Чи краще використовувати, memcpyяк показано нижче, або краще використовувати їх std::copy()з точки зору продуктивності? Чому?

char *bits = NULL;
...

bits = new (std::nothrow) char[((int *) copyMe->bits)[0]];
if (bits == NULL)
{
    cout << "ERROR Not enough memory.\n";
    exit(1);
}

memcpy (bits, copyMe->bits, ((int *) copyMe->bits)[0]);

Зауважте, що це charможе бути підписано або без підпису, залежно від реалізації. Якщо кількість байтів може бути> = 128, тоді використовуйте unsigned charдля своїх байтових масивів. (У (int *)ролях було б і безпечніше (unsigned int *).)
Дан Бреслау

13
Чому ви не використовуєте std::vector<char>? Або , так як ви говорите bits, std::bitset?
GManNickG

2
Власне, чи не могли б ви мені пояснити, що (int*) copyMe->bits[0]робить?
користувач3728501

4
не впевнений, чому щось, що здається таким безладом із таким мало життєвим контекстом, було на рівні +81, але так. @ user3728501 я здогадуюсь, що запуск буфера має intдиктуючий його розмір, але це здається рецептом катастрофи, визначеної реалізацією, як і багато інших речей тут.
підкреслюй_

2
Насправді, цей (int *)склад є просто чистою невизначеною поведінкою, а не визначеною реалізацією. Намагання виконувати набір типу за допомогою каста порушує суворі правила дозволу, а отже, Стандарт не є повністю визначеним. (Крім того, в C ++, хоча це не C, ви не можете вводити каламбур через unionбудь-який.) Насправді єдиним винятком є ​​те, що ви переходите на варіант char*, але надбавка не є симетричною.
підкреслюйте_d

Відповіді:


207

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

Я написав реалізацію C ++ SHA-2. У своєму тесті я хешував 5 рядків, використовуючи всі чотири версії SHA-2 (224, 256, 384, 512), і я цикл 300 разів. Я вимірюю рази за допомогою Boost.timer. Цього лічильника на 300 циклів достатньо, щоб повністю стабілізувати мої результати. Я провів тест по 5 разів кожен, чергуючи memcpyверсію і std::copyверсію. Мій код використовує переваги захоплення даних на якомога більшій кількості фрагментів (багато інших реалізацій працюють з char/ char *, тоді як я працюю з T/ T *(де Tнайбільший тип в реалізації користувача, який має правильну поведінку переповнення), тому швидкий доступ до пам'яті на Найбільші типи, які я можу, є основними в роботі мого алгоритму. Це мої результати:

Час (в секундах) для завершення запуску тестів SHA-2

std::copy   memcpy  % increase
6.11        6.29    2.86%
6.09        6.28    3.03%
6.10        6.29    3.02%
6.08        6.27    3.03%
6.08        6.27    3.03%

Загальне середнє збільшення швидкості std :: копія через memcpy: 2,99%

Мій компілятор - gcc 4.6.3 у Fedora 16 x86_64. Мої прапори з оптимізації є -Ofast -march=native -funsafe-loop-optimizations.

Код моїх реалізацій SHA-2.

Я вирішив також запустити тест на мою реалізацію MD5. Результати були набагато менш стабільними, тому я вирішив зробити 10 пробіжок. Однак після моїх перших спроб я отримав результати, які дивовижно змінювались від одного запуску до другого, тож я здогадуюсь, що там відбувалася якась діяльність ОС. Я вирішив почати спочатку.

Ті ж настройки компілятора та прапори. Є лише одна версія MD5, і вона швидша, ніж SHA-2, тому я зробив 3000 циклів на подібному наборі з 5 тестових рядків.

Це мої останні 10 результатів:

Час (в секундах) для завершення виконання тестів MD5

std::copy   memcpy      % difference
5.52        5.56        +0.72%
5.56        5.55        -0.18%
5.57        5.53        -0.72%
5.57        5.52        -0.91%
5.56        5.57        +0.18%
5.56        5.57        +0.18%
5.56        5.53        -0.54%
5.53        5.57        +0.72%
5.59        5.57        -0.36%
5.57        5.56        -0.18%

Загальне середнє зниження швидкості std :: копія через memcpy: 0,11%

Код для моєї реалізації MD5

Ці результати говорять про те, що існує певна оптимізація, що std :: copy використовується в моїх тестах SHA-2, які std::copyне вдалося використати в моїх тестах MD5. У тестах SHA-2 обидва масиви були створені в одній функції, що викликала std::copy/ memcpy. У моїх тестах на MD5 один із масивів передався функції як функціональний параметр.

Я зробив трохи більше тестування, щоб побачити, що я можу зробити, щоб зробити std::copyшвидше знову. Відповідь виявилася простою: увімкніть оптимізацію часу зв'язку. Це мої результати з увімкненою LTO (опція -flto в gcc):

Час (в секундах) на завершення виконання тестів MD5 за допомогою -flto

std::copy   memcpy      % difference
5.54        5.57        +0.54%
5.50        5.53        +0.54%
5.54        5.58        +0.72%
5.50        5.57        +1.26%
5.54        5.58        +0.72%
5.54        5.57        +0.54%
5.54        5.56        +0.36%
5.54        5.58        +0.72%
5.51        5.58        +1.25%
5.54        5.57        +0.54%

Загальне середнє збільшення швидкості std :: копіювання через memcpy: 0,72%

Підводячи підсумок, за використання не видається покарання за ефективність std::copy. Насправді, схоже, є підвищення продуктивності.

Пояснення результатів

То чому б std::copyце може збільшити продуктивність?

По-перше, я не очікував би, що це буде повільніше для будь-якої реалізації, доки не буде включена оптимізація вбудовування. Всі компілятори вбудовуються агресивно; це, мабуть, найважливіша оптимізація, оскільки вона дозволяє так багато інших оптимізацій. std::copyможе (і я підозрюю, що це реально реалізує у всьому світі) виявити, що аргументи тривіально копіюються і пам'ять викладається послідовно. Це означає, що в гіршому випадку, коли memcpyце законно, std::copyслід виконувати не гірше. Тривіальна реалізація, std::copyяка відкладає, memcpyповинна відповідати критеріям вашого компілятора: "завжди вказуйте це під час оптимізації для швидкості чи розміру".

Однак std::copyтакож зберігає більше своєї інформації. Під час дзвінка std::copyфункція зберігає типи недоторканими. memcpyпрацює на void *, що відкидає майже всю корисну інформацію. Наприклад, якщо я передаю масив std::uint64_t, компілятор або реалізатор бібліотеки, можливо, зможуть скористатися 64-бітним вирівнюванням std::copy, але це може бути складніше memcpy. Багато реалізацій алгоритмів, подібних до цієї роботи, спочатку працюють над неврівноваженою частиною на початку діапазону, потім вирівняною частиною, потім нерівномірною частиною в кінці. Якщо все гарантовано вирівняно, код стане простішим та швидшим, і передбачувач гілок у вашому процесорі стане легшим.

Передчасна оптимізація?

std::copyзнаходиться в цікавому положенні. Я очікую, що це ніколи не буде повільніше, memcpyа іноді і швидше з будь-яким сучасним оптимізуючим компілятором. Більше того, все, що можна memcpy, ти можеш std::copy. memcpyне дозволяє перекривати буфери, тоді як std::copyпідтримує перекриття в одному напрямку ( std::copy_backwardдля іншого напрямку перекриття). memcpyпрацює тільки на покажчики, std::copyпрацює на будь-яких ітератори ( std::map, std::vector, std::deque, або мій власний користувальницький тип). Іншими словами, ви повинні просто використовувати, std::copyколи вам потрібно копіювати фрагменти даних навколо.


35
Хочу наголосити, що це не означає, що std::copyна 2,99% або 0,72% або -0,11% швидше, ніж memcpyці часи для всієї програми. Однак, як правило, я вважаю, що орієнтири в реальному коді корисніші, ніж орієнтири в підробленому коді. Уся моя програма отримала таку зміну швидкості виконання. Реальні наслідки лише двох схем копіювання матимуть більші відмінності, ніж показано тут, якщо брати їх ізольовано, але це свідчить про те, що вони можуть мати вимірні відмінності у фактичному коді.
Девід Стоун

2
Я хочу не погодитися з вашими висновками, але результати - це результати: /. Однак одне запитання (я знаю, що це було давно, і ви не пам’ятаєте дослідження, тому просто коментуйте так, як ви думаєте), ви, ймовірно, не заглядали в код складання;
ST3

2
На мій погляд , memcpyі std::copyмає різні варіанти реалізації, тому в деяких випадках компілятор оптимізує оточуючих код і фактичний код копіювання пам'яті в якості одного цільного шматка коду. Іншими словами, іноді краще, ніж інше, і навіть іншими словами, вирішити, що використовувати - це передчасна або навіть дурна оптимізація, тому що в будь-якій ситуації вам доведеться робити нові дослідження і, тим більше, зазвичай розробляються програми, тож після деякі незначні зміни переваги функції перед іншими можуть бути втрачені.
ST3

3
@ ST3: Я б міг уявити, що в гіршому випадку std::copy- це тривіальна вбудована функція, яка викликає лише те, memcpyколи вона законна. Базовий вкладиш усуне будь-яку негативну різницю в роботі. Я оновлю публікацію з невеликим поясненням, чому std :: копія може бути швидшою.
Девід Стоун

7
Дуже інформативний аналіз. Re Загальне середнє зниження швидкості std :: копіювання за memcpy: 0,11% , хоча число є правильним, результати не є статистично значущими. 95% довірчий інтервал для різниці в засобах становить (-0,013s, 0,025), що включає нуль. Як ви вказували, що в інших джерелах були різні варіанти, і ви, напевно, можете сказати, що ефективність однакова. Для довідки, інші два результати є статистично значущими - шанси побачити різницю у часі цієї крайності випадково приблизно 1 на 100 мільйонів (перший) та 1 на 20 000 (останній).
TooTone

78

Усі компілятори, які я знаю, замінять просту std::copyна a, memcpyколи це доречно, а ще краще, векторизують копію, щоб вона була навіть швидшою, ніж a memcpy.

У будь-якому випадку: профіліруйте і дізнайтеся самі. Різні компілятори будуть робити різні речі, і цілком можливо, він не буде робити саме те, що ви просите.

Дивіться цю презентацію щодо оптимізації компілятора (pdf).

Ось що робить GCC для простого std::copyтипу POD.

#include <algorithm>

struct foo
{
  int x, y;    
};

void bar(foo* a, foo* b, size_t n)
{
  std::copy(a, a + n, b);
}

Ось розбирання (лише -Oоптимізація), показ дзвінка до memmove:

bar(foo*, foo*, unsigned long):
    salq    $3, %rdx
    sarq    $3, %rdx
    testq   %rdx, %rdx
    je  .L5
    subq    $8, %rsp
    movq    %rsi, %rax
    salq    $3, %rdx
    movq    %rdi, %rsi
    movq    %rax, %rdi
    call    memmove
    addq    $8, %rsp
.L5:
    rep
    ret

Якщо ви змінили підпис функції на

void bar(foo* __restrict a, foo* __restrict b, size_t n)

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


1
Як я можу зробити профілювання. Який інструмент використовувати (у Windows та Linux)?
користувач576670

5
@Konrad, ти маєш рацію. Але memmoveце не повинно бути швидше - скоріше, воно повинно бути повільніше, оскільки воно повинно враховувати можливість того, що два діапазони даних перетинаються. Я думаю, що std::copyдозволить збігатися з даними, і тому потрібно викликати memmove.
Чарльз Сальвія

2
@Konrad: Якщо memmove завжди був швидшим, ніж memcpy, тоді memcpy називав bi memmove. Те, що std :: copy насправді може відправляти (якщо що-небудь), визначено реалізацією, тому не корисно згадувати конкретику, не згадуючи про реалізацію.
Фред Нурк

1
Хоча, проста програма для відтворення такої поведінки, складена з -O3 під GCC, показує мені memcpy. Це наштовхує мене на думку, що GCC перевіряє, чи є перекриття пам'яті.
jweyrich

1
@Konrad: стандарт std::copyдозволяє перекриватися в одному напрямку, але не в іншому. Початок виходу не може лежати в межах вхідного діапазону, але початок вводу може лежати в межах вихідного діапазону. Це трохи дивно, оскільки визначено порядок призначення, і виклик може бути UB, навіть якщо ефект цих призначень у цьому порядку визначений. Але я припускаю, що обмеження дозволяє оптимізувати векторизацію.
Стів Джессоп

24

Завжди використовувати std::copyтому , що memcpyобмежується тільки C-стилі POD структур, і компілятор, ймовірно , замінити виклики std::copyз , memcpyякщо цілі, насправді POD.

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


Чому ви хочете копіювати навколо ітераторів?
Амокреації

3
Ви не копіюєте ітераторів, а скоріше діапазон, визначений двома ітераторами. Наприклад, std::copy(container.begin(), container.end(), destination);буде скопійовано вміст container(все між beginі end) в буфер, позначений символом destination. std::copyне вимагає, як шеньюгани, &*container.begin()або &container.back() + 1.
Девід Стоун

16

Теоретично, memcpy може мати невелику , непомітну , нескінченно малу перевагу у виконанні, лише тому, що вона не має таких самих вимог, як std::copy. З чоловічої сторінки memcpy:

Щоб уникнути переповнення, розмір масивів, вказаних як пунктом призначення, так і вихідними параметрами, повинен бути принаймні числом байтів і не повинен перетинатися (для блоків пам'яті, що перекриваються, memmove - більш безпечний підхід).

Іншими словами, memcpyможна ігнорувати можливість перекриття даних. (Передача масивів, що перекриваються, - memcpyце невизначена поведінка.) Тому memcpyне потрібно чітко перевіряти цю умову, тоді як std::copyможна використовувати, доки OutputIteratorпараметр не знаходиться в діапазоні джерела. Зауважте, це не те саме, що говорити про те, що діапазон джерела та діапазон призначення не можуть перетинатися.

Отже, оскільки std::copyє дещо інші вимоги, теоретично це повинно бути трохи (з особливим акцентом на трохи ) повільніше, оскільки воно, ймовірно, перевірить наявність перекриваючих C-масивів або ж делегує копіювання C-масивів доmemmove , для чого потрібно виконати перевірити. Але на практиці ви (і більшість профілів), ймовірно, навіть не виявите різниці.

Звичайно, якщо ви не працюєте з PODs , ви не можете користуватисяmemcpy все одно .


7
Це справедливо для std::copy<char>. Але std::copy<int>можна припустити, що його вхідні дані вирівняні між собою. Це призведе до набагато більшої різниці, оскільки це впливає на кожен елемент. Перекриття - одноразова перевірка.
MSalters

2
@MSalters, правда, але більшість реалізацій memcpyя бачив перевірити вирівнювання та спробувати скопіювати слова, а не байт у байт.
Чарльз Сальвія

1
std :: copy () теж може ігнорувати пам'ять, що перекривається. Якщо ви хочете підтримувати пам'ять, що перекривається, вам доведеться самостійно записати логіку для виклику std :: reverse_copy () у відповідних ситуаціях.
Цигон

2
Можна зробити зворотний аргумент: при переході через memcpyінтерфейс він втрачає інформацію про вирівнювання. Отже, memcpyпотрібно робити перевірки вирівнювання під час виконання, щоб обробити нерівномірні початку та цілі. Ці чеки можуть бути дешевими, але вони не безкоштовні. Тоді як std::copyможна уникнути цих перевірок і векторизувати. Крім того, компілятор може довести, що масиви джерела та призначення не перетинаються та знову векторизуються, без того, щоб користувач мав вибір між memcpyі memmove.
Максим Єгорушкін

11

Моє правило просте. Якщо ви використовуєте C ++, віддайте перевагу бібліотекам C ++, а не C :)


40
C ++ був явно розроблений, щоб дозволити використовувати C-бібліотеки. Це було не випадково. Часто краще використовувати std :: copy, ніж memcpy в C ++, але це не має нічого спільного з тим, що є C, і такий аргумент зазвичай є неправильним підходом.
Фред Нурк

2
@FredNurk Зазвичай ви хочете уникнути слабкої області C, де C ++ забезпечує більш безпечну альтернативу.
Phil1970

@ Phil1970 Я не впевнений, що C ++ у цьому випадку набагато безпечніший. Нам залишається пройти дійсні ітератори, які не перевищують і т. Д. Я вважаю, що можливість використовувати std::end(c_arr)замість c_arr + i_hope_this_is_the_right_number_of elementsних безпечніше? а може, ще важливіше, зрозуміліше. І в цьому конкретний випадок я наголошу: std::copy()це ідіоматичніше, більш досяжне, якщо типи ітераторів змінюються пізніше, призводять до чіткішого синтаксису тощо.
підкреслюється

1
@underscore_d std::copyє більш безпечним, оскільки він правильно копіює передані дані, якщо вони не є типом POD. memcpyбуде щасливо скопіювати std::stringоб’єкт у новий байт представлення за байтом.
Єнс

3

Лише незначне доповнення: різниця швидкостей між memcpy()і std::copy()може досить різнитися залежно від того, активізовано чи вимкнено оптимізацію. З g ++ 6.2.0 і без оптимізацій memcpy()чітко виграє:

Benchmark             Time           CPU Iterations
---------------------------------------------------
bm_memcpy            17 ns         17 ns   40867738
bm_stdcopy           62 ns         62 ns   11176219
bm_stdcopy_n         72 ns         72 ns    9481749

Коли ввімкнено оптимізацію ( -O3), все знову виглядає приблизно так само:

Benchmark             Time           CPU Iterations
---------------------------------------------------
bm_memcpy             3 ns          3 ns  274527617
bm_stdcopy            3 ns          3 ns  272663990
bm_stdcopy_n          3 ns          3 ns  274732792

Чим більший масив, тим менш помітний ефект отримує, але навіть при N=1000 memcpy()приблизно вдвічі швидшому, коли оптимізація не вмикається.

Вихідний код (потрібен тест Google):

#include <string.h>
#include <algorithm>
#include <vector>
#include <benchmark/benchmark.h>

constexpr int N = 10;

void bm_memcpy(benchmark::State& state)
{
  std::vector<int> a(N);
  std::vector<int> r(N);

  while (state.KeepRunning())
  {
    memcpy(r.data(), a.data(), N * sizeof(int));
  }
}

void bm_stdcopy(benchmark::State& state)
{
  std::vector<int> a(N);
  std::vector<int> r(N);

  while (state.KeepRunning())
  {
    std::copy(a.begin(), a.end(), r.begin());
  }
}

void bm_stdcopy_n(benchmark::State& state)
{
  std::vector<int> a(N);
  std::vector<int> r(N);

  while (state.KeepRunning())
  {
    std::copy_n(a.begin(), N, r.begin());
  }
}

BENCHMARK(bm_memcpy);
BENCHMARK(bm_stdcopy);
BENCHMARK(bm_stdcopy_n);

BENCHMARK_MAIN()

/* EOF */

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

3
@bolov Не завжди. У деяких випадках важливо мати відносно швидку програму під налагодженням.
Жолудь

2

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

Можна зробити багато для оптимізації копіювання пам'яті - ще більше, якщо ви готові використовувати для цього кілька потоків / ядер. Див., Наприклад:

Чого не вистачає / недооптимально в цій програмі memcpy?

і питання, і деякі відповіді пропонують реалізацію або посилання на реалізацію.


4
режим педанта: зі звичайним застереженням, що " використовувати жодне з них " означає, що якщо ви довели, що у вас є конкретна ситуація / вимога, щодо якої жодна стандартна функція, що надається вашою реалізацією, не є достатньо швидкою ; в іншому випадку, моє звичайне занепокоєння полягає в тому, що люди, які не довели, що потрапляють в сторону передчасної оптимізації копіювального коду замість зазвичай більш корисних частин програми.
підкреслюй_

-2

Профілювання показує, що твердження: std::copy()завжди так швидко, як memcpy()і швидше, помилкове.

Моя система:

HP-Compaq-dx7500-Microtower 3.13.0-24-generic # 47-Ubuntu SMP Пт 2 травня 23:30:00 UTC 2014 x86_64 x86_64 x86_64 GNU / Linux.

gcc (Ubuntu 4.8.2-19ubuntu1) 4.8.2

Код (мова: c ++):

    const uint32_t arr_size = (1080 * 720 * 3); //HD image in rgb24
    const uint32_t iterations = 100000;
    uint8_t arr1[arr_size];
    uint8_t arr2[arr_size];
    std::vector<uint8_t> v;

    main(){
        {
            DPROFILE;
            memcpy(arr1, arr2, sizeof(arr1));
            printf("memcpy()\n");
        }

        v.reserve(sizeof(arr1));
        {
            DPROFILE;
            std::copy(arr1, arr1 + sizeof(arr1), v.begin());
            printf("std::copy()\n");
        }

        {
            time_t t = time(NULL);
            for(uint32_t i = 0; i < iterations; ++i)
                memcpy(arr1, arr2, sizeof(arr1));
            printf("memcpy()    elapsed %d s\n", time(NULL) - t);
        }

        {
            time_t t = time(NULL);
            for(uint32_t i = 0; i < iterations; ++i)
                std::copy(arr1, arr1 + sizeof(arr1), v.begin());
            printf("std::copy() elapsed %d s\n", time(NULL) - t);
        }
    }

g ++ -O0 -o test_stdcopy test_stdcopy.cpp

профіль memcpy (): головний: 21: зараз: 1422969084: 04859 минув: 2650 us
std :: copy () профіль: головний: 27: зараз: 1422969084: 04862 минув: 2745 us
memcpy () минув 44 s std :: copy ( ) минуло 45 с

g ++ -O3 -o test_stdcopy test_stdcopy.cpp

профіль memcpy (): головний: 21: зараз: 1422969601: 04939 минув: 2385 us
std :: copy () профіль: головний: 28: зараз: 1422969601: 04941 минув: 2690 us
memcpy () минуло 27 с std :: copy ( ) минуло 43 с

Red Alert вказав, що код використовує memcpy з масиву в масив і std :: копіювати з масиву в вектор. Цей куд буде приводом для швидшого заповнення.

Оскільки є

v.резерв (sizeof (arr1));

не повинно бути різниці в копії у вектор або масив.

Код закріплений для використання масиву для обох випадків. memcpy все ще швидше:

{
    time_t t = time(NULL);
    for(uint32_t i = 0; i < iterations; ++i)
        memcpy(arr1, arr2, sizeof(arr1));
    printf("memcpy()    elapsed %ld s\n", time(NULL) - t);
}

{
    time_t t = time(NULL);
    for(uint32_t i = 0; i < iterations; ++i)
        std::copy(arr1, arr1 + sizeof(arr1), arr2);
    printf("std::copy() elapsed %ld s\n", time(NULL) - t);
}

memcpy()    elapsed 44 s
std::copy() elapsed 48 s 

1
неправильно, ваше профілювання показує, що копіювання в масив швидше, ніж копіювання у вектор. Не по темі.
Червоне сповіщення

Я можу помилитися, але у вашому виправленому прикладі з memcpy ви не копіюєте arr2 в arr1, тоді як за допомогою std :: copy ви копіюєте arr1 в arr2? ... Що ви можете зробити, це зробити кілька, чергування експерименти (один раз партія memcpy, один раз партія std :: copy, потім знову назад з memcopy тощо), кілька разів. Тоді я б використовував clock () замість часу (), тому що хто знає, чим міг би займатися ваш ПК на додаток до цієї програми. Всього два мої центи, хоча ... :-)
paercebal

7
Отже, перехід std::copyвід вектора до масиву якимось чином memcpyзайняв майже вдвічі довше? Ці дані є дуже підозрілими. Я компілював ваш код за допомогою gcc з -O3, і згенерована збірка однакова для обох циклів. Тож будь-яка різниця у часі, яку ви спостерігаєте на своєму апараті, є лише випадковою.
Червона
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.