Блог пользователя pavook

Автор pavook, история, 22 месяца назад, перевод, По-русски

Иногда я задумываюсь, какую реализацию дерева отрезков написать в задаче. Обычно я при помощи метода "пальцем в небо" выбираю какую-то и в большинстве случаев она проходит ограничения.

Я решил подвести основу, так сказать базу, под этот выбор и протестировал на производительность 4 разные реализации:

  • Простой рекурсивный "Разделяй и властвуй"
    Код
  • Оптимизированный рекурсивный "Разделяй и властвуй", который не спускается в заведомо ненужных сыновей.
    Код
  • Нерекурсивная реализация (взял отсюда: https://codeforces.com/blog/entry/18051)
    Код
  • Дерево Фенвика
    Код

Все реализации поддерживают такие запросы:

  • get(l, r): сумма на отрезке (полуинтервале) $$$[l; r)$$$
  • add(i, x): прибавление к элементу под индексом $$$i$$$ числа $$$x$$$

Вот результаты:

Примечание: я старался не делать никаких оптимизаций, требовательных к конкретным запросам, чтобы с небольшими изменениями структуры данных могли применяться для любых операций.

Я генерировал запросы следующим образом:

  • Прибавление в точке: случайный индекс (rnd() % size) и случайное число
  • Сумма на отрезке: сначала, генерируется длина отрезка (rnd() % size + 1), затем подходящая левая граница.

Исходники бенчмарка. Примечание: желательно отключить CPU frequency scaling, закрыть все приложения, которые могут мешать бенчмарку (чем больше закроете -- тем в теории стабильнее будет результат) и "прибить" процесс к конкретному CPU.

Скрипт на Python, создающий красивый график

Результаты в формате JSON на случай, если Вы захотите ещё поиграться с данными.

Я компилировал бенчмарк с #pragma GCC optimize("O3") на GNU GCC 11.3.0, запускал его с фиксированной частотой процессора 2.4 GHz, прикрепив к конкретному ядру процессора.

Наверное, это мой первый вклад в сообщество, поэтому любые дополнения/предложения приветствуются.

  • Проголосовать: нравится
  • +126
  • Проголосовать: не нравится

»
22 месяца назад, # |
  Проголосовать: нравится 0 Проголосовать: не нравится

Auto comment: topic has been updated by pavook (previous revision, new revision, compare).

»
22 месяца назад, # |
  Проголосовать: нравится 0 Проголосовать: не нравится

Автокомментарий: текст был обновлен пользователем pavook (предыдущая версия, новая версия, сравнить).

»
22 месяца назад, # |
  Проголосовать: нравится +17 Проголосовать: не нравится

All good, but benchmark have 1 error: Fenwick != Segment tree

  • »
    »
    22 месяца назад, # ^ |
      Проголосовать: нравится 0 Проголосовать: не нравится

    Although it's technically true, Fenwick tree and non-recursive segment tree are similar both in structure and in performance. It's also a frequent "dilemma": implementing Fenwick tree or segment tree, so its addition felt appropriate.

    • »
      »
      »
      22 месяца назад, # ^ |
        Проголосовать: нравится 0 Проголосовать: не нравится

      I do not consider the Fenwick tree and the non-recursive segment tree to be similar in structure.

    • »
      »
      »
      22 месяца назад, # ^ |
        Проголосовать: нравится 0 Проголосовать: не нравится

      Even if it's true, you still can't perform many types of complex operations using Fenwick tree, so imho Fenwick tree is quite useless for regular contests... Anyway, thanks for the blog, it was really interesting :)

      • »
        »
        »
        »
        22 месяца назад, # ^ |
          Проголосовать: нравится +3 Проголосовать: не нравится

        Fenwick tree is useful when TL is tight or (if the task allows) if writing a Segment tree will be too long.

»
22 месяца назад, # |
  Проголосовать: нравится -12 Проголосовать: не нравится

I think a pointer-based segment tree is missing.

»
22 месяца назад, # |
  Проголосовать: нравится +14 Проголосовать: не нравится

Thanks for the job you have already done!

However, in my option, it doesn't provide any useful information. It's more a toy research project as you eventually learn different segment tree implementation than a serious benchmark because it simply says something like recursive > non-recursive > fenwick as expected with g glance.

To improve, I list several possible direction here:

  • The asymptotic complexity is $$$O(n \log n)$$$ with derivatives $$$\log n + 1 > 0$$$ which means the curve should be convex. However, the plot shows somehow counter-intuitively a concave curve. I suggest to plot against $$$\mathrm{time} / n \log n$$$ or use log-scaled $$$n$$$-axis which may end up with a plausible result or find serious drawdark of the existing plotting.
  • Try to generate more testsuits instead of simply randomly generated data. For example, is there any carefully crafted data point which causes significant performance downgrade, like to introduce unexpected high ratio of cache misses?
  • Try to compare different segment tree operations besides simple additions. How does the complexity of the basic operations affects the overall speed?
  • Simply fixing the cpu frequency and turning off graphical environment does not mean sufficient. First of all, what's your CPU model? and what's the instruction set? Did it run in the full-power mode? Did you pin cpu core to avoid context switching? And we know that segment tree presents large amount of memory access. So you should also provides the information about memory. I think it's better the breakdown the part into cpu computation and memory accessing, and carefully measure metrics like cache misses.
  • »
    »
    22 месяца назад, # ^ |
    Rev. 2   Проголосовать: нравится 0 Проголосовать: не нравится

    Thank you very much for the suggestions.

    1. I think L2/L3 cache sizes had their impact on this result along with other things like branch misses. Here are the plots divided by $$$\log n$$$:
      Plots

      Notably, non-recursive query has a remarkably constant constant :).

      The sudden jumps in update constant plot you can see (at $$$N \approx 275000$$$ for recursive implementations, at $$$N \approx 450000$$$ for non-recursive implementation) align quite nicely with tree sizes beginning not to fit in my 8M L3 Cache.

      I couldn't figure anything else special about the plots, so any help/ideas would be appreciated.

    2. I don't know even how to approach this suggestion. I'm sure it's very hard to figure out performance-intensive tests with pure random and I'm too stupid for evolution or simulated annealing approaches.
    3. I think I will do this eventually. I'll try to post updates somewhere here.
    4. CPU model: Intel(R) Core(TM) i5-1135g7. I did run it in full-power mode (perf-bias set to 0 and perf-policy set to performance). I reran the benchmark with pinning the process to a CPU core using taskset, thank you for this advice.

      About cache misses and other advanced metrics: I feel like that information would be quite a pain in the butt to collect. I don't know how to measure those metrics except for tools like perf. But if I'm going use perf or something similar, I'll need to run all the instances separately and collect the results separately. That would really blow up the complexity of running the benchmark.

»
22 месяца назад, # |
  Проголосовать: нравится 0 Проголосовать: не нравится

Автокомментарий: текст был обновлен пользователем pavook (предыдущая версия, новая версия, сравнить).

»
22 месяца назад, # |
  Проголосовать: нравится 0 Проголосовать: не нравится

Auto comment: topic has been updated by pavook (previous revision, new revision, compare).