FizzBuzz по-сениорски

- Добрый день, я на интервью на позицию старшего разработчика.

- Здравствуйте, давайте начнем с небольшого теста, пока я ваше CV смотрю. Напишите программу, которая выводила бы числа от 1 до, скажем, миллиарда, притом если число кратно трем, то вместо числа выводится Fizz, если кратно пяти, то Buzz, а если и трем, и пяти, то FizzBuzz.

Серьезно, FizzBuzz? Задачка для начальной школы, на сениорскую позицию? Ну ладно.


Я достаю свой верный лаптоп, и пишу такой код:

#include <stdio.h>

#define LIMIT 1000000000

int main(void) {
    for (int i = 1; i <= LIMIT; i++) {
        if (0 == i % 3) {
            if (0 == i % 5) {
                printf("FizzBuzz\n");
            } else {
                printf("Fizz\n");
            }
        } else if (0 == i % 5) {
            printf("Buzz\n");
        } else {
            printf("%d\n", i);
        }
    }

    return 0;
}

Запускаю программу, она себе бежит, но не так чтобы сильно быстро, через 3 с чем-то минуты, после первого миллиона, я ее прерываю и быстренько высчитываю, что весь процесс займет больше двух суток. Да, наверно надо было включить буферизацию, это бы несколько ускорило, но это не спасет, лучше просто перенаправить вывод в файл, что я и делаю, и через 41.5 секунду у меня есть красивенький файл на 7.5 гигов.

- Вам не кажется, что можно побыстрее? - спрашивает интервьюер.

- Да ладно, основное время занимает I/O, 7.5 гигов записать — не шутка, даже на SSD.

- А давайте перенаправим вывод в /dev/null.

- Без проблем.

Через минуту:

- Как это — 39.5 секунд? То есть весь I/O занимает 2 секунды, а все остальное время — мой код?

- Да, так получается. Это не самая медленная реализация, на каждой итерации два сравнения и один printf, я часто вижу вариант с тремя сравнениями и двумя printf’ами. Для джуниора, я бы сказал, это даже хорошо. А вот для сениора ...

Это было больно, но, пожалуй, заслужено. Ладно, я тебе покажу, кто тут джуниор.

- Сейчас сделаю побыстрее.

- Попробуйте. Только объясняйте, что вы делаете.

- Видите, что у нас тут есть паттерн — каждые 3*5, то есть 15 итераций цикла логика полностью повторяется. Тогда можно переделать цикл:

    for (i = 1; i < LIMIT - 15; i += 15) {
        printf( "%d\n"          // 1
                "%d\n"          // 2
                "Fizz\n"        // 3
                "%d\n"          // 4
                "Buzz\n"        // 5
                "Fizz\n"        // 6
                "%d\n"          // 7
                "%d\n"          // 8
                "Fizz\n"        // 9
                "Buzz\n"        // 10
                "%d\n"          // 11
                "Fizz\n"        // 12
                "%d\n"          // 13
                "%d\n"          // 14
                "FizzBuzz\n",   // 15
                i, i+1, i+3, i+6, i+7, i+10, i+12, i+13);
    }

- Если раньше на каждые 15 чисел у нас приходилось 15 сравнений переменной цикла и два if’а в теле цикла, то есть в общей сложности 45 сравнений, а каждое сравнение — это потенциальная проблема с branch prediction’ом, то теперь одно. Да и вызовов printf’а стало в 15 раз меньше. Одна только проблема — цикл не дойдет ровно до миллиарда, а только до 999999990 (макс число, кратное 15 и меньшее миллиарда), так что оставим старый цикл, но только для обработки хвоста, то есть последних 10 значений (это практически не влияет на производительность).

После всех изменений получился такой код.

- И что у нас со временем получается?

- Если вывод в файл, то 22.5 секунды, если в /dev/null – 20.2

Интервьюер доволен, похоже, чего-то такого он от меня и ожидал. Но … зря он про джуниора сказал.

- Я думаю, что это не предел.

- В самом деле? А что тут можно еще оптимизировать?

- Я уменьшил количество вызовов printf’а в 15 раз, но при этом сами эти printf’ы стали тяжелее. Да и вообще printf сам по себе тяжелый, из-за своей мощности — это ведь фактически виртуальная машина со своим языком, полным по Тьюрингу, на нем даже крестики-нолики писали. В данной ситуации используется лишь небольшая часть возможностей printf, так что можно его заменить на что-то свое, более легкое:

#define NUM cur += myitoa(num++, cur)
#define FIZZ do { memcpy(cur, "Fizz\n", 5); cur += 5; num++; } while (0)
#define BUZZ do { memcpy(cur, "Buzz\n", 5); cur += 5; num++; } while (0)
#define FIZZBUZZ do { memcpy(cur, "FizzBuzz\n", 9); cur += 9; } while (0)

void print(int num) {
    static char wrkbuf[CHUNK_SIZE];

    char *cur = wrkbuf;
    NUM;
    NUM;
    FIZZ;
    NUM;
    BUZZ;
    FIZZ;
    NUM;
    NUM;
    FIZZ;
    BUZZ;
    NUM;
    FIZZ;
    NUM;
    NUM;
    FIZZBUZZ;
    fwrite(wrkbuf, cur - wrkbuf, 1, stdout);
}

- Можно, конечно, использовать уже готовую itoa, но это нестандартная функция, не везде есть, да и она слишком универсальная, поскольку поддерживает разные системы счисления, а у нас только десятичная система - упрощаем все, что можно. Ну и, конечно, в главном цикле просто вызываем print(i) вместо длинного printf’а.

Получается такой код.

Я подхожу к доске и рисую табличку с результатами запусков:

Вариант

Вывод в файл

Вывод в /dev/null

Время (сек)

Относ наивной

Относ предыдущей

Время (сек)

Относ наивной

Относ предыдущей

наивная

41.429

1x

-

39.650

1x

-

оптимизация цикла

22.546

1.83x

1.83x

20.151

1.97x

1.97x

отказ от printf

12.563

3.30x

1.80x

8.771

4.52x

2.30x

- В принципе на вывод в файл можно особо не смотреть — там какое-то время съедается на I/O, и оно плавает, так что лучше ориентироваться на время без I/O.

Я стираю ту часть, где про вывод в файл.

- Итого ускорение в 4 с половиной раза.

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

- Я знаю, как можно еще ускорить.

- Серьезно?

- Абсолютно. Я до этого использовал чисто технические способы ускорения, а ведь можно еще и алгоритмически улучшить. Смотрите, что будет напечатано, когда мы вызываем, например, print(150000001) и следующий за ним print(150000016):

150000001\n150000002\nFizz\n150000004\nBuzz\nFizz\n150000007\n150000008\nFizz\nBuzz\n150000011\nFizz\n150000013\n150000014\nFizzBuzz\n
150000016\n150000017\nFizz\n150000019\nBuzz\nFizz\n150000022\n150000023\nFizz\nBuzz\n150000026\nFizz\n150000028\n150000029\nFizzBuzz\n
       ^^         ^^               ^^                     ^^         ^^                     ^^               ^^         ^^ 

- Отличия всего в 16 байтах, а программа всю строку пересобирает с нуля. Можно просто менять байты на месте. Правда, заранее неизвестно, сколько десятичных разрядов надо поменять, так что это потребуется вычислить — сравнить два буфера, и определить, где они отличаются. Это, пожалуй, тяжеловатая задача, но у нас есть, — я делаю театральную паузу — векторные инструкции и интринсики для них!

Я не озвучиваю, но подразумеваю, что джуниор такого бы не придумал. В этот момент понимаю, что интервьюер тоже.

Открываю Intel’овскую страницу с интринсиками и нахожу там нужные векторные функции для работы с 16-байтными векторами. У меня тут максимум 10-байтные, но их можно добить нулями до 16, не проблема. И да, 16-байтные вектора — это SSE инструкции, никакой AVX-512 тут не нужен, мой 4-летний мобильный проц это точно потянет.

Получаю такой кусок с жирными и вкусными интринсиками:

unsigned int diff = 0xFFFF & ~_mm_movemask_epi8(_mm_cmpeq_epi8(
                                  _mm_load_si128((__m128i const *)prev_first_number),
                                  _mm_load_si128((__m128i const *)last_number)));
unsigned int diff_pos = 16 - _tzcnt_u32(diff);   // number of changed digits

Быстрая проверка flags в /proc/cpuinfo – нужные для выбранных мной интринсиков SSE2 (еще со времен Pentium 4) и BMI1 (появился в Haswell’ах) в CPU есть, все должно работать.

Запускаю тот код, что получился, смотрю уже только вывод в /dev/null и обновляю табличку:

Вариант

Время (сек)

Относительно наивной

Относительно предыдущей

наивная

39.650

1x

-

оптимизация цикла

20.151

1.97x

1.97x

отказ от printf

8.771

4.52x

2.30x

переиспользование буфера

4.490

8.83x

1.95x

Еще почти в 2 раза ускорились! А по сравнению с начальным вариантов так вообще почти в 9. Жаль, до 10 раз не дотянул.

- Ну все, наверно теперь уже хватит. Это уже вполне по-сениорски.

Во взгляде интервьюера читается облегчение.

- Скорее всего, мы вплотную подошли к пределу того, что можно выжать из однопоточной программы, - говорю я, медленно подвисая к концу фразы. Как же я об этом раньше не подумал! - Я ведь еще многопоточность не попробовал!

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

- Я буду выделять каждому рабочему потоку кусок числового поля, и этот поток будет возвращать готовый буфер для своего куска, а главный поток будет печатать эти буферы, соблюдая порядок:

for (int j = 0; j < THREAD_COUNT; j++) {
        thread_pool[j].start_num = i;
        thread_pool[j].count = NUMS_PER_THREAD;
        thread_pool[j].buf = malloc(BUFFER_SIZE);
        pthread_create(&thread_pool[j].id, NULL, worker, (void *)&thread_pool[j]);
        i += NUMS_PER_THREAD;
    }
    int active_threads = THREAD_COUNT;
    int max = LIMIT / 15 * 15;
    for (int j = 0; active_threads; j = (j+1) % THREAD_COUNT) {
        pthread_join(thread_pool[j].id, NULL);
        fwrite(thread_pool[j].buf, thread_pool[j].buflen, 1, stdout);
        if (max - i > NUMS_PER_THREAD) {
            thread_pool[j].start_num = i;
            pthread_create(&thread_pool[j].id, NULL, worker, (void *)&thread_pool[j]);
            i += NUMS_PER_THREAD;
        } else if (max > i) {
            thread_pool[j].start_num = i;
            thread_pool[j].count = max - i + 1;
            pthread_create(&thread_pool[j].id, NULL, worker, (void *)&thread_pool[j]);
            i += max - i + 1;
        } else {
            free(thread_pool[j].buf);
            active_threads--;
        }
    } 

- У меня двухядерный проц с гипертредингом, так что четыре рабочих потока одновременно будет оптимально, пока главный поток занимается выводом, один рабочий поток не используется, так что в любой момент времени максимум 4 потока активны. Конечно, стоит поэксперементировать с размером куска, который дается на обработку — в идеале рабочий поток должен обрабатывать свой кусок ровно за то же время, что главный поток выводит данные, тогда никто никого не ждет, все работают на максимальной скорости.

Проведя несколько замеров, я остановился на кусках по 3 миллиона чисел — удобное число, кратное 15, и результат хороший.

Получился такой код.

Запускаю, и обновляю данный в табличке:

Вариант

Время (сек)

Относительно наивной

Относительно предыдущей

наивная

39.650

1x

-

оптимизация цикла

20.151

1.97x

1.97x

отказ от printf

8.771

4.52x

2.30x

переиспользование буфера

4.490

8.83x

1.95x

многопоточность

1.748

22.68x

2.57x

- Ну вот, я уменьшил время обработки в 22 с лишним раза. И код получился очень даже сениорский — умный алгоритм, многопоточность, интринсики опять же. Как считаете?

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

Я быстро закрыл лаптоп и покинул офис. Почему-то мне так и не перезвонили.


Все тесты делались на Dell Latitude 7480 с i7-7600U 2.8 Ghz, 16 Gb памяти, SSD и OpenSUSE Leap 15.1 с kernel’ом 4.12.14, каждый тест не менее 10 раз, выбиралось наименьшее значение. При компиляции использовались флаги -O3 -march=native -pthread

Все варианты кода производят одинаковый результат, когда вывод направлен в файл, то есть работают корректно. Код доступен в этом репо.

Реклама
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее

Комментарии 169

    +1
    одинаковый результат, когда вывод направлен в файл, то есть работают корректно.

    А это точно означает «корректно»? Может, просто «одинаково неправильно»? :)
      +22
      Я проверял — одинаково правильно :)
      Просто проверять каждый раз 7.5-гиговый файл довольно муторно, я проверил первый, а дальше просто сверял размер и контрольные суммы
      +19
      Но это же только оптимизация по быстродействию. А что с занимаемой памятью, что с размером самой программы, что с нагрузкой на процессор? Ведь оптимизировать можно по разным критериям. А то так можно просто блобом запихнуть результирующий файл в программу и просто записывать его на диск — проверять лень, но возможно выйдет ещё быстрее.
        +31

        Вот да, я ожидал развязку в стиле "мы сделаем всё на шаблонах, чтобы итоговый FizzBuzz вычислился во время компиляции, а работа программы будет заключаться исключительно в том, чтобы вывести строку из секции .data в консоль." :D

          +28
          Но ведь это медленнее — прочитать 7.5GB из .data (с диска), чем на лету сгенерировать.
            –3

            Если структурировать данные, и ввести индексы, то быстро)

              0
              Самый быстрый способ — это заранее записать файл на диск и сделать к нему жёсткую ссылку.
                +1
                Сделать виртуальную ФС, в которой данные генерятся на лету.
                  0
                  Нет, задача FizzBuzz не в записи файла на диск, а в генерации потока в stdout. Возможность записи на диск — опциональная.
                    0
                    А я и не предлагаю запись на диск:)
                      0
                      То есть, программа берёт данные из виртуальной ФС и копирует в stdout? А её запуск сводится к банальной cat? Интересно, действительно интересно.
                        0
                        Именно.
            +3
            Ну если подходить чисто формально, то задачу ставит тот, кто интервьюирует. А он про память ничего не спрашивал — следовательно, требованиям по памяти программа удовлетворяет. Вообще, насколько я понимаю, автор их если и увеличил на второй и последующих итерациях, то не сильно, просто потому, что никаких аллокаций, особенно крупных, в коде не видно.
              +6
              Более-менее используется память в последнем варианте — для каждого потока выделяется буфер в 3M * 119 байт / 15 = 22.7 Mb, для 4 потоков выходит 91 Mb. Все остальные варианты очень скромные по памяти.
                0
                А, ну да — многопоточный вариант наверное съест больше.
              +1
              Если у вас отпимизация по быстродействию то в результате нагрузка на CPU максимальна. Если же процессор простаивает, то значит что то вы недогрузили и перформанс не максимальный.
              Размер программы, соглашусь, критерий независимый, но это не точно :)
            +46
            Поставили задачу: «написать FizzBuzz».
            Ожидают результата: «полёт на Марс».
              +6
              А человек построил межзвёздный корабль.
                +1
                Но ему всё равно сказали, мы вам перезвоним.
                  +2
                  и начал тонко тюнить гипердрайв…
                +26

                Любой более менее опытный человек может загрузить любого другого опытного человека узкоспециализированной задача на который первый съел собаку

                  +58

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

                    +3
                    В статье есть и про мотивацию почему автор не остановился вовремя.

                    Для джуниора, я бы сказал, это даже хорошо. А вот для сениора…

                    Это было больно, но, пожалуй, заслужено. Ладно, я тебе покажу, кто тут джуниор.
                      0

                      Так и запишем: ставит эмоции выше рациональности

                      +3

                      плюс нежелание остановиться после команды работодателя, с политической точки зрения может расцениваться как вариант деструктивного поведения. (с программной точки зрения это — полное зависание процесса) ;)

                      +50
                      А не взяли вас, так как клепать формочки вам было бы скучно
                        +10
                        Скорее потому, что разбирать такой код будет слишком долго и муторно для команды. В итоге, будет потом очень больно увольнять специалиста, написавшего такой код.
                          +7
                          Если нужна прям быстрота-быстрота, то код по-любому будет какой-то такой, кто его ни напиши.
                            +4
                            согласен с предыдущим оратором, просто вызов такого метода нужно сделать как черный ящик и написать все нужные комментарии — тогда будет читаемо.

                            Многие же, к сожалению, думают — раз я такой крутой программист что пишу так, что джуны не понимают — мой код самодокументирующийся. А на самом деле — чем больше опыта, тем проще и понятнее пишешь код.
                              0

                              Черные ящики на практике обычно оказываются не такими уж и черными, и каждая последующая версия хоть и быстрее но тем сложнее оптимизируется. Что если мы сделаем FizzBuzzFooBar? в превоначальном варианте это ещё два ифа, во втором нужно будет константу менять и заполнять пробелы формата выводом чисел, а про последующие даже думать не хочу. Что делать когда варианты перестают влезать не только в 10 байт, но и вообще в AVX512?


                              Такой код имеет право на жизнь, безусловно, очень крутая статья, свой плюсик я там нарисовал. Но не надо сениора который остановился на первом (втором?) варианте считать хоть чем-то хуже: минимум у него другие интересы, а как максимум он просто смог оценить что решение удовлетворительно и оптимизировать наносекунды ради катастрофического падения простоты кода не нужно

                            +8
                            Сдаётся мне, что не взяли потому что первая пустая формочка была бы получена только через два месяца.
                              +6
                              И заменяла бы собой весь сайт:)
                            –2
                            В реальной жизни программа FizzBuzz написанная Сеньором на собеседовании, скорее всего, выглядела бы еще короче и оптимальнее — «Ciao!»
                              +4
                              Почему? Статья отличный пример того, как можно проверять уровень сеньора на простых задачах.
                              +55

                              Как я понял, главное при написании FizzBuzz — надёжно заблокировать выход из переговорки.

                                +11
                                Не увидел в статье претендента на сеньёра реализацию FizzBuzz на ассемблере (32/64)
                                с многопоточным использованием. :)

                                P.S. А, если серьёзно, cтатья просто суперская! Спасибо.
                                FizzBuzz on rosettacode.org
                                Ну, и немного отвлечённого Уроки от NeHe на masm64 — программирование задач с OpenGL на ассемблере.

                                P.P.S. Минусаторы, можете выдохнуть, этот комментарий не для вас!

                                Пользователи новомодных Фреймворков и языков программирования «курят» нервно в стороне когда вопрос решения задачи «максимально» отзывчив для пользователей и занимает немного в размере результирующего бинарника. :) (imho)
                                  +4
                                  «ASM – это чудесно», – думал я когда-то. Но что Вы будете делать со своим кодом лет через 5-10, если половина ноутбуков перейдёт на ARM?
                                    –2

                                    как будто на ARM нет ассемблера :)

                                      +3

                                      Есть конечно. Что Вы будете делать со всем Вашим кодом, который Вы за эти 5-10 лет написали под x86_64?

                                        +3

                                        Переписывать за сеньорскую зарплату!

                                          –2
                                          а «минусаторы» считают, что нет :)
                                            +1

                                            "Минусаторы", думаю, негодуют из-за того, что вы упорно не понимаете, что дело не в наличии или отсутствии ассемблера.

                                              0

                                              они не понимают иронии. И, видимо, из тех, кто "программист на [%your_programming_language_name%]".
                                              Смена технологий так сильно пугает, что "фсёпропало"?

                                                0

                                                Я за последнее время перепробовал толпу языков, постоянно пишу на трех совершенно разноплановых (шарп, раст, тс), и могу сказать что я лично больше верю в свою возможность сообщить нужную инфу компилятору чтобы он сделал что нужно. Да, я могу возиться с vtune и считать такты, но я никогда не полезу писать ассемблер, а подсуну интринсики/likely/… чтобы компилятор сам сделал то, что мне нужно. И обычно он куда умнее меня, а ещё он никогда* не ошибается.


                                                Как итог: когда компилятор становится умнее, прогрмма оптимизируется автоматически, под любую платформу собирается максимум с некоторой потерей производительности, а минимум и без неё, потому что быстрый код написанный в таком стиле часто быстрый везде, просто по факту "думали прежде чем писали".


                                                В общем, каждому своё, какой-нибудь asmbb за верх искусства воспринимать мне тяжело.

                                        +1
                                        Это точно. И все x86_64 интринсики отправятся туда же. Хотя вроде как у ARM есть какие-то vector extensions. Но переписывать под них по-любому придется.
                                          +1
                                          1. Да, у ARM есть neon
                                          2. Обычно интринсики — это не очень большая часть кода, заменить их гораздо более быстрая задача, чем переписать программу на ASM целиком.

                                          Но в целом, конечно, векторные инструкции и правда не то место, где портируемость C сильно помогает.

                                          +1

                                          https://github.com/DLTcollab/sse2neon


                                          Некоторые костыли, конечно, но всё сразу с нуля переписывать не потребуется.

                                          +1
                                          Получается, претендент пропустил важный первый шаг: поискать готовое решение в Интернет?
                                            0
                                            Обычно bottleneck не в таких алгоритмах (в либах — мб, но не в обычном коде). Скорее всего, на событии скрола на 1 пиксель создаются объекты, много вьюшек с прозрачностью одна на одной или блочится главный поток. По крайней мере мы, пользователи нативных мобильных фрэймворков, заточены решать проблемы именно такого класса, т.к. именно в них 99% проблем с производительностью.
                                              +4
                                              Есть жизнь и за пределами фронт-энда
                                                +1
                                                топикстартер писал про фронт, и я написал про фронт. Да и ваши задачи обычно давно решены, и боттлнек в отсутствии нужного индекса или ненужном джоине, а не в алгоритмах. К сожалению, время велосипедов прошло.
                                                  0

                                                  Поэтому у нас в штате есть программист-алгоритмист который читает пейперы и тюнит ядро всей нашей продуктовой линейки в vtune выискивая такты. Пойду завтра ему скажу что его не существует. И нет, это ядро обычного вебсайта, с докерами, местами нодами, постгрями-монгами и прочим. Ну то есть обычный веб бекенд, но только есть там один апп, который в бд не ходит, имеет всего пару хттп ручек и б0льшую часть времени проводит в числодробилке с матрицами.

                                            +23
                                            Мне кажется для сениора в такой реализации важно было бы спросить, а зачем это нужно и как используется этот код?
                                              +15
                                              зачем такие извращения если уже давно есть готовое решение на java? github.com/EnterpriseQualityCoding/FizzBuzzEnterpriseEdition

                                              /sarcasmoff
                                                –9
                                                я бы не позвонил, потому что понял что передо мной обычный сноб, лучше простой парень, чем такой «сеньор-помидор»
                                                  +31
                                                  На каждую саркастически-юмористическую статью найдется сеньор-помидор, который верит в реальность и серьёзность происходящего =)
                                                    +11
                                                    Тяжело жить без хаба «Юмор».
                                                  +2
                                                  Спасибо за задачку, было интересно подумать над ней!

                                                  Я взял за основу customprint.c (работает примерно 9.2 сек на моей машине), и применил следующие идеи:

                                                  — Экономить на вызовах fwrite. Для этого процессим числа в блоках, кратных 15. Я остановился на 35000 блоках. Дает небольшой выигрыш.
                                                  — Поддерживать текущее число в виде строки и эмулировать инкремент. Ускоряет за счет того, мы не всегда итерируемся по всем цифрам текущего числа (гораздо чаще инкрементится только последняя цифра).
                                                  — Экономить на инкрементах. Для этого заметим, что если после числа мы следующим выведем слово, то можно инкрементить на сразу 2, если два слова — то на 3.
                                                  — Мелкие микрооптимизации, которые почти ни на что не повлияли (например, полное избавление от деления и взятия по модулю при инкременте)

                                                  Итоговое решение работает за примерно 3.4 сек на моей машине (ссылка)

                                                    0
                                                    > Экономить на вызовах fwrite. Для этого процессим числа в блоках, кратных 15. Я остановился на 35000 блоках. Дает небольшой выигрыш

                                                    Сомневаюсь, что это дает какой-то выигрыш, поскольку fwrite() уже буферизован. Возможно, если поиграть с размером буфера, используя setvbuf(), можно что-то выиграть, но это какие-то сущие копейки будут.

                                                    > Поддерживать текущее число в виде строки и эмулировать инкремент. Ускоряет за счет того, мы не всегда итерируемся по всем цифрам текущего числа (гораздо чаще инкрементится только последняя цифра).

                                                    Примерно эта идея и была использована в reusebuffer.c, только сразу для всего буфера.

                                                    > Мелкие микрооптимизации, которые почти ни на что не повлияли (например, полное избавление от деления и взятия по модулю при инкременте)

                                                    В customprint.c нет деления по модулю, я от него избавился шагом раньше, в unrolled.c (ну если не считать обработки хвоста, то его нет смысла хоть как-то оптимизировать, цикл на 10 итераций).
                                                      0
                                                      > Сомневаюсь, что это дает какой-то выигрыш, поскольку fwrite() уже буферизован.
                                                      Я тоже так думал, но был удивлен, когда это дало примерно 1 сек выигрыша. Думаю передавать много маленьких буферов в fwrite хуже, чем буферы побольше.

                                                      Преверить можно установив константу CHUNK_COUNT в 1.

                                                      > В customprint.c нет деления по модулю
                                                      Он был нужен мне в инкременте. Как говорится сам добавил, сам же и соптимизировал.

                                                        0
                                                        > Думаю передавать много маленьких буферов в fwrite хуже, чем буферы побольше.

                                                        По идее не должно, поскольку fwrite буферизует вывод, т.е. далеко не каждый вызов fwrite() приводит к syscall'у write(), который, конечно, довольно дорогой.

                                                        > Преверить можно установив константу CHUNK_COUNT в 1.

                                                        Попробую, интересно. Я в процессе своих экспериментов играл с размером буфера, и не обнаружил статистически значимой разницы, то есть стандартного буферу размера BIFSIZ, который используется для (f)printf/(f)puts/fwrite/etc, вполне хватает.
                                                        Возможно, тут сказывается какая-то разница в параметрах системы.

                                                        Вы попробуйте вместо этого цикла в 35000 итераций задать буфер в 5 MB через setvbuf — будет ли разница? Если будет, то тогда к fwrite есть вопросы, конечно.

                                                        Хотя у меня есть одно предположение — буферизация функций стандартной библиотеки обязательно должна быть thread-safe (а у меня ведь все компилируется с -pthread), так что скорее всего для сериализации доступа к единому буферу во всех этих функциях, в т.ч. и в fwrite(), используются mutex'ы, а блокировка/разблокировка mutex'а — это syscall'ы, переход из userspace в kernelspace, и это дорого. Как вариант, можно попробовать скомпилировать этот вариант без -pthread и посмотреть, изменится ли что-то.
                                                          0
                                                          Поигрался с размером буфера, никаких изменений не заметил. 5МБ буфера и 1 чанк выдали 4.8 сек, против 3.4 при 35000 чанках. Компиляция без -pthread и замена fwrite на fputs_unlocked тоже ничего не дали.

                                                          Если что, использовал это:
                                                          setvbuf(stdout, NULL, _IOFBF, 5242880);

                                                        0
                                                        Про буферизацию: т.е. не имеет смысла в наивном варианте на шагах ветвления заполнять буфер, чтобы потом сделать один вызов printf?
                                                          0
                                                          Для вывода на консоль — имеет, потому как по умолчанию тогда построчная буферизация, в остальных случаях наверно нет.
                                                          В любом случае нет смысла городить свою буферизацию, потому что в стандартной библиотеке C она уже есть, максимум — ее надо включить/поменять размер буфера, используя setbuf/setvbuf.
                                                      +7
                                                      История с собеседованием реальная или наложена на идею развития fizzBuzz?
                                                        +29
                                                        Ну наконец-то :)

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

                                                        Был еще один вариант, многопоточный с memory-mapped I/O, но он оказался сильно медленнее обычного многопоточного, я думаю, это вызвано тем, что когда маппируешь в память 7.5 гигов, и потом пишешь туда, то количество page fault'ов такое, что их обработка kernel'ом обходится очень дорого. Эта версия косвенно подтверждается тем, что использование huge pages (2M-байтных), вместо стандартных 4K-байтных заметно ускорило процесс, но все равно до обычного многопоточного варианта было далеко.
                                                          +5
                                                          Класс! Код читал через раз (ну не программист :), но ваши пояснения логики действий отлично добавляли ясности, ну и драматизма :).
                                                          Вы не пробовали писать «технологические рассказы»? Ниша имхо свободна, последний раз ощущение схожее с ощущением, возникшим от вашей статьи у меня было от «мне не хватало одного байта» :)
                                                            +5
                                                            Спасибо :)

                                                            > Вы не пробовали писать «технологические рассказы»?
                                                            Вы удивитесь, но в числе моих хобби есть и такое. Надеюсь, что когда-то это доберется до публики :)
                                                              +3
                                                              Как там говорится...«неистово плюсуем!» :)
                                                                +3

                                                                удивился, прочитав "неистово плюсуем!" в обсуждении текста без кода на C++
                                                                :))

                                                            0
                                                            А можно небольшой гайд об использовании huge pages? Моя упорно не хочит маппить на них. Возможно, я чтото упускаю в системных настройках.
                                                              0
                                                              Для этого должна быть включена поддержка в kernel'е. В /proc/meminfo надо найти Hugepagesize, и при маппинге использовать именно этот размер.
                                                              В mmap'овском man'е написано, как правильно надо задавать флаг с размером huge page, там нужно правильный сдвиг сделать.
                                                          0
                                                          myitoa можно ускорить, развернуть цикл и писать сразу в выходной буфер вместо копирования
                                                            0
                                                            Согласен, можно избавиться от лишнего memcpy. Примерно что-то такое реализовано на следующем шаге.
                                                            0
                                                            Интересно, а есть какие-то операции сверхбыстрого сложения массивов (наложение дампов памяти)?

                                                            Если да, наверное, можно попробовать подготовить относительно небольшие дампы (числа по возрастанию добитые пробелами до одного размера, Fizz/Buzz/FizzBuzz, дополнительные десятичные разряды, символы переноса строки). И потом накладывая их друг на друга с правильными смещениями собирать итоговый массив как из конструктора. Или нет?

                                                            p.s. Кстати, операцию наложения массивов можно перенести на видеокарту. Вот где простор для оптимизаций))
                                                              +3

                                                              На самом деле, критерии "синьорности" зависят от компании, и в данном конкретном случае старший разработчик реально будет заниматься микро оптимизациями и выжимать производительность из каждой инструкции, но обычно это немного не так.
                                                              На мой взгляд, старший разработчик должен был бы скорее написать код (даже для FizzBuzz), который хоть как-то можно тестировать, расширять и сопровождать. Возможно даже с тестами, документацией и проверками пред- и пост- условий. Только не надо шуток про корпоративную версию FizzBuzz.

                                                                +3
                                                                Иногда приходит product owner и говорит, что надо сделать вот такую-то штуку минимум в 5 раз быстрее, иначе в ней нет смысла. И тогда начинаются такие танцы с бубном, когда на калькуляторе считаешь, что влезает, а что не влезает в кэш процессора, и где можно избавится от лишнего if'а, чтобы branch prediction не обижать, и всякое такое. И о том, как это поддерживать, будем думать потом. Это тоже одна из сторон жизни, не самая приятная, но это не значит, что ее нет.
                                                                  +2

                                                                  Безусловно так может получиться. Я поэтому и написал, что критерии для определения старшего разработчика разнятся от компании к компании.
                                                                  Основной мой посыл был в любом случае в том, что старший разработчик обычно видит и держит в уме больше деталей связанных с жизненным циклом продукта, его архитектурой и развитием системы. Безусловно, для этого есть другие типы заданий и секции собеседований, но и по FizzBuzz можно многое сказать. Допустим, если я по какой-то странной причине попросил бы кандидата на должность старшего разработчика писать FizzBuzz, меня бы уже очень сильно насторожило что реализация, как в статье, не вынесена в отдельную функцию, да ещё и сверху добавлен макрос (!) с очень общим именем (!!).

                                                                    +1

                                                                    Наоборот — самая приятная! :)

                                                                      0
                                                                      Мсье знает толк в извращениях :D
                                                                  +17
                                                                  Вы извините, к вам тут зашёл тестировщик и обнаружил, что программа работает корректно не для всех N, а только для N кратных размеру буфера…

                                                                  Reopened…
                                                                    +2
                                                                    Подумал, что надо расширить коммент, а то заминусуют же…

                                                                    Фразу «Напишите программу, которая выводила бы числа от 1 до, скажем, миллиарда...» надо читать примерно так:
                                                                    Наши аналитики провели исследование и пришли к выводу, что среднему пользователю будет достаточно вывода в миллиард строк. Поэтому давайте пока в качестве MVP сделаем так, а потом посмотрим на метрики, пособираем обратную связь и по результатам будем решать, как дальше развивать фичу.

                                                                    А после торжественного выпуска фичи на прод события будут развиваться примерно так:
                                                                    1. Прибегут пользователи с пожеланиями сделать настраиваемым число выводимых строк. Найдутся те, кому миллиард — это очень много («Для нашего маленького бизнеса за глаза будет достаточно от 1 до 1000»), и будет крупняк, которому подавай вывод в секстильон строк, а то и в два.
                                                                    2. Прибегут эксплуататоры с вопросом: «А что фича жрет так много ресурсов впустую? Нам говорят, что большинству миллиард строк совсем не нужен, а вы всем так отдаёте!»
                                                                    3. Прибегут маркетологи со словами: «Ой, фича такая популярная, но жрёт столько ресурсов (эксплуататоры жалуются). Давайте её тарифицировать! Сделаем так, что на бесплатном тарифе будет доступен вывод 1..K строк, на базовом — 1..L строк, а на VIP-тарифе все 1..M строк!»

                                                                    А ваш код к этим котелкам совсем не готов. Переписывать слишком много придётся…
                                                                      +2
                                                                      Есть ещё вероятность, что можно заложиться на все эти кейсы, чтобы в будущем не больно было поддерживать и дорабатывать, а потом фича лежит и не используется вообще никем и никак.
                                                                        0
                                                                        Вот полностью согласен. Часто с этим сталкивался, приходилось напоминать людям про ru.wikipedia.org/wiki/YAGNI
                                                                        Особенно прикольно, если фичей не будут пользоваться потому, что она слишком тормозная.
                                                                          0

                                                                          Во-первых делать расширяемым код под всевозможные будущие хотелки — невозможно.
                                                                          Во-вторых это такой же антипаттерн как хардкод одного сценария, я бы назвал его "бесхребетный" код, который состоит из одних адаптеров-стратегий-репозиториев которые из пустого впорожнее переливают с миллиардом никогда не изменяемых опций. Посмотрите на физзбазз на Java из примеров выше — о да, вот уж где можно расширить и изменить что угодно. Явно подготовились к любой хотелке!

                                                                      0

                                                                      Вполне, однако, если мы /dev/null пишем, то это легко проверить, и тогда программа выполняется почти мговенно. А если не в /dev/null, то тут время io решает.


                                                                      Шах и мат.

                                                                        +3

                                                                        Смотря какие требования. Я бы не стал оптимизировать производительность любой ценой в ущерб читаемости кода.

                                                                          –10

                                                                          Да, если senior на C (!) пишет такой неоптимальный первый вариант — это повод задуматься.


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


                                                                          #include <stdio.h>
                                                                          
                                                                          int main()
                                                                          {
                                                                              for (int i = 0, f = 0; i < 1000000000; ++i)
                                                                              {
                                                                                  if (!(i % 3)) printf("F"), f = 1;
                                                                                  if (!(i % 5)) printf("B"), f = 1;
                                                                                  if (f) printf("\n"), f = 0;
                                                                              }
                                                                          }

                                                                          Выше — самый простой и очевидный вариант (как раз тот "наивный вариант", от собеседуемого ожидаемый), который даёт (CPU такой же, проверил с Fizz/Buzz, да, там 28 секунд, за счёт длины строк):


                                                                          gcc x.c &&  time ./a.out > /dev/null                                                                                                            
                                                                          ./a.out > /dev/null  14,83s user 0,12s system 99% cpu 15,031 total

                                                                          После этого (при желании — до) senior должен спросить, каковы, собственно, требования к решению, а не заниматься бездумной оптимизацией.
                                                                          И только потом уже предлагают оптимизации, которыми являются:


                                                                          • Уменьшение совокупного количества системных вызовов.
                                                                          • Использование оптимизаций компилятора (как написать код, не мешающий компилятору).
                                                                          • Возможные способы алгоритмической оптимизации без сильного ущерба читаемости.

                                                                          Дальше — многопоточность с map->reduce.


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

                                                                            +4
                                                                            Вы проверили скорость, но не проверили ответ…

                                                                            Ваша реализация решает другую задачу: она не печатает пропущенные числа (а должна).
                                                                              –6

                                                                              Во-первых, это в данном случае, не столь важно: главное то, как должен выглядеть первичный вариант.
                                                                              Во-вторых, если вы посмотрите изначальное условие, которое было дано автором в статье, то ничего подобного не увидите (т.е., нет прямого указания на вывод числа, если оно не кратно, хотя возможно предположить из условия, что это подразумевается):


                                                                              — Здравствуйте, давайте начнем с небольшого теста, пока я ваше CV смотрю. Напишите программу, которая выводила бы числа от 1 до, скажем, миллиарда, притом если число кратно трем, то вместо числа выводится Fizz, если кратно пяти, то Buzz, а если и трем, и пяти, то FizzBuzz.

                                                                              В-третьих, "просто добавьте else" или, например, так:


                                                                              ...
                                                                                  if (f)
                                                                                  {
                                                                                      printf("\n");
                                                                                      f = 0;
                                                                                      continue;
                                                                                  }
                                                                              
                                                                                 printf("%d\n", i);
                                                                              ...
                                                                                +2
                                                                                Так добавление else к условию кардинально замедляет выполнение программы.
                                                                                На моём железе и с моим gcc (всё собрано с O3) самый простой вариант автора (который первый) работает чуть меньше 35 секунд, а Ваш с continue (и полными строками) — чуть больше 45 секунд, разница больше четверти в пользу авторского простого решения.
                                                                                  –4

                                                                                  Да, насчёт замеров вы правы, здесь я ошибся.


                                                                                  И чисто теоретически, любопытно: видимо, компилятор оптимизирует повторные вычисления (при желании, возможно посмотреть листинг того, что он нагенерил).
                                                                                  Без оптимизаций авторский вариант у меня выполняется за 70-71с:


                                                                                   ➭ gcc x.c &&  time ./a.out > /dev/null                                                                                                            
                                                                                  ./a.out > /dev/null  70,03s user 0,76s system 99% cpu 1:10,94 total

                                                                                  Мой же с continue за 81с. Автор немного выигрывает за счёт else if, и в таком варианте, я получаю 78 с.:


                                                                                  ...
                                                                                          if (0 == i % 3) printf("Fizz"), f = 1;
                                                                                          else if (0 == i % 5) printf("Buzz"), f = 1;
                                                                                  
                                                                                          if (!f)
                                                                                          {
                                                                                              printf("%d\n", i);
                                                                                              continue;
                                                                                         }
                                                                                  
                                                                                         printf("\n");
                                                                                         f = 0;
                                                                                  ...

                                                                                  Но основный выигрыш автор за счёт числа системных вызовов: в случае двойной кратности, он сразу печатает "FizzBuzz", я же всегда делаю отдельный вызов для печати каждого слова.


                                                                                  И это всё-равно не меняет сути комментария выше.


                                                                                  Сам первичный код у автора написан запутанно и не оптимально (если хотите формально — у него цикломатическая сложность немного выше, чем нужно, при этом, задача на оптимизацию ещё не стояла).
                                                                                  И более простой код, пусть и чуть более медленный, предпочтительней.
                                                                                  Когда же, начинается оптимизация, как я писал в комментарии выше, возможно уменьшать число системных вызовов:


                                                                                      enum PrintType { pt_none = 0x0, pt_fizz = 0x1, pt_buzz = 0x2, pt_fizzbuzz = 0x3 };
                                                                                  
                                                                                      for (int i = 0; i < 1000000000; ++i)
                                                                                      {
                                                                                          switch (!(i % 3) | (!(i % 5) << 1))
                                                                                          {
                                                                                              case pt_fizz: printf("Fizz\n"); break;
                                                                                              case pt_buzz: printf("Buzz\n"); break;
                                                                                              case pt_fizzbuzz: printf("FizzBuzz\n"); break;
                                                                                              default: printf("%d\n", i); break;
                                                                                          }
                                                                                     }
                                                                                  

                                                                                  Результат:


                                                                                  ➭ gcc x.c &&  time ./a.out > /dev/null                                                                                                            
                                                                                  ./a.out > /dev/null  69,39s user 0,68s system 99% cpu 1:10,17 total

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

                                                                                    0
                                                                                    ./a.out > /dev/null  70,03s
                                                                                    ./a.out > /dev/null  69,39s

                                                                                    Теряюсь в догадках — а сработала ли оптимизация?

                                                                                  +10
                                                                                  Напишите программу, которая выводила бы числа

                                                                                  нет прямого указания на вывод числа

                                                                                  А вы точно синьор?
                                                                                    +1
                                                                                    Во-первых, это в данном случае, не столь важно: главное то, как должен выглядеть первичный вариант.

                                                                                    Мы Вам перезвоним :)
                                                                                      0
                                                                                      нет прямого указания на вывод числа, если оно не кратно, хотя возможно предположить из условия, что это подразумевается


                                                                                      Напишите программу, которая выводила бы числа от 1 до, скажем, миллиарда
                                                                                      И как же должно выглядеть более прямое указание?
                                                                                        –3
                                                                                        И как же должно выглядеть более прямое указание?

                                                                                        По крайней
                                                                                        Лучше, как явная приписка:


                                                                                        "притом если число кратно трем, то вместо числа выводится Fizz, если кратно пяти, то Buzz, а если и трем, и пяти, то FizzBuzz, в ином случае выводится само число".


                                                                                        Но да, меня вчера переглючило.
                                                                                        Оправдаюсь хотя бы тем, что собеседования обычно не проводят в два часа ночи. :-)

                                                                                    –2
                                                                                    Выше — самый простой и очевидный вариант

                                                                                    За такой "простой и очевидный вариант" следует сразу лицо бить.

                                                                                      +1
                                                                                      > За такой «простой и очевидный вариант» следует сразу лицо бить.

                                                                                      Бить лицо — перебор, но вариант с тремя if'ами и двумя printf'ами на КАЖДУЮ итерацию действительно очень плох, я бы сильно задумался, брать ли такого даже джуниора, потому как его придется слишком много чему учить.
                                                                                        +1
                                                                                        потому как его придется слишком много чему учить.

                                                                                        Когда много учить — это еще хорошо. Проблема — когда научить нельзя. Можно научить писать человека быстрый код — это делается элементарно. Но вот, например, научить человека отличать хороший код от плохого — решительно невозможно, т.к. вкус у человека либо есть, либо его нет. И когда человек вот такое:


                                                                                        if (!(i % 3)) printf("F"), f = 1;
                                                                                        if (!(i % 5)) printf("B"), f = 1;
                                                                                        if (f) printf("\n"), f = 0;

                                                                                        называет "более простым, предпочтительным" кодом, по сравнению с "запутанным и неоптимальным" оригиналом — то это конец.


                                                                                        но вариант с тремя if'ами и двумя printf'ами на КАЖДУЮ итерацию действительно очень плох

                                                                                        Плох по сравнению с чем именно? Вопросы оптимизации кода, вообще говоря, это не вопросы сениору, это вопросы мидлу. А сениор от мидла отличается тем, что после просьбы оптимизировать оригинальное решение скажет — "не стану, это не нужно".

                                                                                          +1
                                                                                          > называет «более простым, предпочтительным» кодом, по сравнению с «запутанным и неоптимальным» оригиналом — то это конец

                                                                                          Ну может не конец, но очень нехорошо, согласен.

                                                                                          > Плох по сравнению с чем именно?

                                                                                          По сравнению с начальным вариантом, где два if'a и один printf на итерацию. Я говорю чисто об алгоритмической сложности, не о лишней переменной и не об использовании comma оператора в таком контексте — это отдельный серьезный грех.
                                                                                            0
                                                                                            По сравнению с начальным вариантом, где два if'a и один printf на итерацию. Я говорю чисто об алгоритмической сложности

                                                                                            А я вам говорил про цикломатическую сложность.
                                                                                            Правда, я её неправильно посчитал вчера, так что с этой точки зрения, оба варианта одинаковы (сложность 4).


                                                                                            Модифицированная же лучше у варианта switch/case (тут по Маккейбу, 5 из-за цикла):


                                                                                            ➭ pmccabe *.c
                                                                                            5       5      ...       3if.c: main
                                                                                            5       5       ...      orig.c: main
                                                                                            3       5       ...      switches.c: main

                                                                                            Алгоритмически же они всё-таки O(n), хотя и различаются константой.


                                                                                            Но всё же читается проще вариант без вложенных условий, а в таком примере, — это первое, на что бы я обратил внимание.


                                                                                            и не об использовании comma оператора в таком контексте — это отдельный серьезный грех.

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

                                                                                              +1

                                                                                              В одном вы правы точно: насчёт "лишних" вычислений я вчера написал херню, некорректно сравнил время, а также неправильно прикинул цикломатическую сложность.
                                                                                              В 2 часа ночи, видимо, лучше хотя бы пытаться заснуть, чем сидеть и что-то делать, когда не спится, т.к. делать не подумав — это не правильно…
                                                                                              Ваш пример выглядит сложнее, но он более оптимален, а формально по сложности он с 3if совпадает.

                                                                                              0
                                                                                              Проблема — когда научить нельзя.

                                                                                              И вы это всё увидели по FizzBuzz?


                                                                                              Но вот, например, научить человека отличать хороший код от плохого — решительно невозможно, т.к. вкус у человека либо есть, либо его нет.
                                                                                              И когда человек вот такое:

                                                                                              называет "более простым, предпочтительным" кодом, по сравнению с "запутанным и неоптимальным" оригиналом — то это конец.

                                                                                              Не придираясь к запятым, вы хотите сказать, что "вариант с тремя ифами" читается сложнее кода, в котором есть вложенные условия?


                                                                                              Вопросы оптимизации кода, вообще говоря, это не вопросы сениору, это вопросы мидлу. А сениор от мидла отличается тем, что после просьбы оптимизировать оригинальное решение скажет — "не стану, это не нужно".

                                                                                              Это вопрос не к "мидлу" или "сениору", разница между ними в том, что последний в контексте решения задачи бизнеса понимает, что сказать, и это не обязательно будет "не стану".

                                                                                              0
                                                                                              вариант с тремя if'ами и двумя printf'ами на КАЖДУЮ итерацию действительно очень плох

                                                                                              Это самый легко читающийся вариант решения "в лоб". Повторюсь, задачи оптимизировать изначально не стояло.
                                                                                              При этом, if — всего-лишь сравнение и переход.


                                                                                              И как-бы там вообще не требуется многократное получение остатка через условия, что я и отметил (но это как-то пропустили, заминусовав):


                                                                                              flag = (!(i % 3) | (!(i % 5) << 1))
                                                                                                +3
                                                                                                > При этом, if — всего-лишь сравнение и переход.

                                                                                                if — не «всего лишь», это очень дорогая операция в случае, когда branch predictor ошибается, что приводит к перегрузке всего конвейера. Согласно Wiki: Modern microprocessors tend to have quite long pipelines so that the misprediction delay is between 10 and 20 clock cycles en.wikipedia.org/wiki/Branch_predictor

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

                                                                                                Вариант со switch'ем действительно интересный, я его еще в том комменте заметил. Тут, конечно, у branch predictor'а шансов угадать почти нет, зато только один раз на цикл, надо будет проверить, какой он даст выигрыш. Сдвиг + ИЛИ + 2 НЕ сильно дешевле одного misprediction'а.
                                                                                                  0

                                                                                                  Предсказание работает достаточно хорошо, и в большинстве случаев, конвейер не будет перезагружен.
                                                                                                  У Intel там совсем не не тривиальный проприетарный алгоритм предсказания.
                                                                                                  Хотя, конечно, соглашусь, что лишнее условие при оптимизации, стоит убрать.
                                                                                                  А switch, по сути, такой же if/else if, он в такой же cmp+je/jne выродится, так что бранч предиктор работать будет одинаково, т.к. он про него вообще не знает.


                                                                                                  Также, начиная с C++-20, к switch/case возможно применять likely/unlikely.
                                                                                                  Соответственно, в gcc возможно с __builtin_expect (макросами likely/unlikely в C) поэкспериментировать.


                                                                                                  Выражение же, я так думаю, возможно ещё оптимизировать, надо только подумать, как.
                                                                                                  Например, заменой div на сложения, используя признаки делимости, которые здесь вспомнили:


                                                                                                  Признак делимости на 3 в двоичной системе счисления звучит следующим образом: «Число делится на 3 тогда и только тогда, когда сумма его цифр стоящих на четных местах отличается от суммы цифр, стоящих на нечетных местах, на число, делящееся на 3».
                                                                                                    0

                                                                                                    У нас есть программа где скорость работы ВСЕГО приложения на 20% изменяется в лучшую сторону при простановки LIKELY в трёх ифах в разных местах. Так что насчет "Достаточно хорошего предсказания" — бабка надвое сказала.


                                                                                                    Мерили со статистикой критерионом.

                                                                                              0
                                                                                              За такой "простой и очевидный вариант" следует сразу лицо бить.

                                                                                              Интересный у вас подход. И как, много людей к вам идут на собеседование? И много уходят с разбитым лицом?

                                                                                            0

                                                                                            В вот наконец то, когда последний вариант готов, настала пора заглянуть в performance counters процессора :) Вы, кстати, не смотрели — куда упираетесь? В память упираться вроде рановато.

                                                                                              +1
                                                                                              Это уже неизвестный мне уровень магии :)
                                                                                              Как это посмотреть? Есть какой-то гайд на эту тему?

                                                                                              Я подозреваю, что упирается все в память, syscall'ов в результате получается не так много. Кстати, появилась странная мысль попробовать отключить всякие Meltdown и Spectre mitigations в ядре, чтобы переключение user space-kernel space быстрее было, и сравнить.
                                                                                                +1

                                                                                                Intel vTune Amplifier (есть триал), если попроще.
                                                                                                https://github.com/andikleen/pmu-tools — если под линуксом и хочется упороться на-отличненько. Вот статья, с которой можно начать: https://halobates.de/blog/p/262 .


                                                                                                Про память — не должно бы в неё упираться, потому что всего в районе 5 гбайт/с записи, к тому же линейной — кэш процессора должен порулить тут.

                                                                                                  0
                                                                                                  > хочется упороться на-отличненько

                                                                                                  Очень хочется. Спасибо, займусь на досуге.

                                                                                                  > Про память — не должно бы в неё упираться, потому что всего в районе 5 гбайт/с записи, к тому же линейной — кэш процессора должен порулить тут.

                                                                                                  Разве кэш как-то помогает при записи в память? Чтения тут почти нет. 5 Gb/s при общем размере вывода в 7.5 Gb дают около 1.5 сек — очень близко к тому, во что я уперся.
                                                                                                    +1
                                                                                                    Разве кэш как-то помогает при записи в память?

                                                                                                    Обычно мешает при больших объемах, даже специальные комманды ввели для пересылки данных в обход кэша MOVNTDQ, что бы более эффективно использовать шину памяти.
                                                                                              0
                                                                                              #define FIZZ do { memcpy(cur, «Fizz\n», 5); cur += 5; num++; } while (0)

                                                                                              вот тут 3 наверно
                                                                                                +1

                                                                                                здесь 5 — это длина строки “Fizz\n”

                                                                                                  +1
                                                                                                  Это же сдвиг указателя на буфер. «Fizz\n» таки 5 байт длиной.
                                                                                                  0
                                                                                                  А как же openmp? Не, до сеньора не дотягивает…
                                                                                                    +1
                                                                                                    А OpenMP даст выигрыш по сравнению с обычным использованием потоков? Я не знаю, никогда с ним не работал, но мне всегда казалось, это просто еще один уровень абстракции над родными для платформы потоками, и соответственно дополнительные расходы
                                                                                                      0
                                                                                                      А OpenMP даст выигрыш по сравнению с обычным использованием потоков?


                                                                                                      Главный выигрыш openmp в том, что код становится значительно проще, понятней и легче в сопровождении.
                                                                                                        0
                                                                                                        Это субъективно. Я привык к Pthread'ам, мне они кажутся вполне понятными, особенно когда не надо париться про синхронизацию, как в этом случае.
                                                                                                          0

                                                                                                          Вот вы пишете "значительно проще, понятней и легче", а я слышу слово "оверхед" :)

                                                                                                      +1
                                                                                                      Я отвернулся от доски и увидел, что в переговорке я один. Через стеклянную стенку переговорки я разглядел интервьюера, который что-то быстро говорил секретарю, показывая пальцем в мою сторону. Он что, вызывает охрану? Похоже, что мне тут больше не рады.

                                                                                                      А что если всё более прозаично — например, у автора есть привычка вроде «ругать пятиэтажным матом всё вокруг» или «вести себя странно» в момент сосредоточенного обдумывания задачи, чего он сам может не заметить и что на самом деле было причиной такого поведения интервьюера? Вот представьте картину со стороны интервьюера: даёте вы человеку задачу — он задумался, ушел глубоко в свои размышления и внезапно начал задумчиво корчить рожи, грызть листочек, почесывать ногой нос и вообще лезть на потолок. Может интервьюер после увиденного не охрану звал а экзорциста;) Это конечно всё шутка, но хотелось бы отметить — я на своем примере знаю, что когда я слишком сосредоточен, то не отдаю себе отчёт в совершаемых действиях (примеры: пошел налить кофе — насыпал кофе в сахарницу и выбросил кружку в мусорное ведро. Или в процессе обдумывания разгрыз карандаш в щепки с угрожающим выражением лица, и т.п.)
                                                                                                        0

                                                                                                        хуже, если собеседуемый явным образом начинает проведение определённых ритуалов для погружения в задачу (раскуривает трубку, стучит в бубен, ритмично совершает высокоамплитудные колебания своим телом и т.п.) :)))

                                                                                                          +1
                                                                                                          Если это дает результат — почему нет :)
                                                                                                          Только посадить его отдельно, чтобы джуниоров не пугать.
                                                                                                            +3
                                                                                                            в комнату с мягкими стенами =)
                                                                                                        +5
                                                                                                        Все таки от сеньора, на мой взгляд, ожидается совсем другое. Читая эту статью, я ждал что после получения задания FizzBuzz, наш сеньор ПРЕЖДЕ чем начать писать код остановится, и начнет задавать вопросы собеседующему: какие ограничения по производительности, какие ограничения по объему памяти, требования к качеству кода и т.д. В общем — напишет маленькую спецификацию для данной задачи, а потом решит её самым простым подходящим способом.

                                                                                                        Знание технических тонкостей, показанные в статье — это конечно замечательно, но по моему не главное в сеньоре.
                                                                                                          +3

                                                                                                          и сначала напишет тесты и документацию :)

                                                                                                            +1
                                                                                                            есть жизнь и вне секты «без ТЗ не подходите» :)
                                                                                                              0
                                                                                                              А на собеседовании откажут по причине: «мы нанимаем сеньора, чтобы он сам всё придумал, а не нас озадачивал»
                                                                                                                0

                                                                                                                Соглашусь, однако возможно этой конторе именно такой сениор и нужен. В конце концов, базы данных и браузеры кому-то писать надо, не боги таки точают. А гнаться кроме необходимых узких технавыков ещё и за софтскиллами стратегическим видением — можете не закрыть вакансию оочень долго

                                                                                                                0
                                                                                                                Можно подключить видеокатрточку.
                                                                                                                Тогда (без учёта времени настройки и загрузки плис) все вычисления и получение готовой строки будет занимать одну-две команды чтения из памяти.
                                                                                                                Ну и запустить на каждое ядро свой поток.
                                                                                                                Тут тормоз будет только в самом stdout

                                                                                                                Если вывод на какую то физику, типа диска, то сделать мап для файла записи и зарядить dma. Тут скорость определится скоростью диска и размером кеша на нем.
                                                                                                                  0
                                                                                                                  Пробовал memory-mapped I/O — оказалось медленнее. Думаю, из-за расходов ядра на обработку page fault'ов, коих на 7.5-гиговом куске памяти будет немало
                                                                                                                  +1
                                                                                                                  Всем привет. Это мой первый комментарий на Хабре. Я очень давно хотел это сделать и поэтому немного переживаю.

                                                                                                                  О себе заранее хочу сказать, что я начинающий python разработчик, с опытом менее трех месяцев и возможно ошибаюсь, заранее прошу прощения за это.

                                                                                                                  Собственно касаемо самой статьи, а что если посчитать количество Fizz, Buzz и FizzBuzz в первой тысяче. А потом просто умножить на число кратное необходимому?

                                                                                                                  Например, если в первой тысяче Fizz, Buzz и FizzBuz каждый встречается по 10 раз, то получается в миллионе оно будет встречаться по 10.000 раз, а в миллиарде по 10.000.000 раз? Получается вместо пересчета можно всего миллиарда можно посчитать первую тысячу, и выводить просто кратные результаты?

                                                                                                                  Например, если Fizz встречается в первой тысяче в числах 10, 100, 110, 210, 310, 410, 510, 610, 710, 910. То логическим продолжением будет 1010, 1100, 1210… 1910 и так далее?
                                                                                                                    +3
                                                                                                                    Помимо Fizz, Buzz и FizzBuzz надо еще выводить числа.

                                                                                                                    > Например, если в первой тысяче Fizz, Buzz и FizzBuz каждый встречается по 10 раз, то получается в миллионе оно будет встречаться по 10.000 раз, а в миллиарде по 10.000.000 раз? Получается вместо пересчета можно всего миллиарда можно посчитать первую тысячу, и выводить просто кратные результаты?

                                                                                                                    Это некорректно, поскольку ни тысяча, ни миллион не кратны 15. Вот если взять полторы тысячи и полтора миллиона, тогда да, эта логика сработает. Но, как сказано выше, числа тоже надо выводить.
                                                                                                                      +2

                                                                                                                      Так там в какой-то момент вычисления (определение делимости) вообще уже не производятся, т.к. через каждые 15 чисел ситуация повторяется. Вопрос почти сразу сводится к скорости вывода.

                                                                                                                      0
                                                                                                                      Самый простой способ оптимизации — это писать в буффер и только его печатать, так как печать в консоль очень сильно замедляет программу, хоть я и на C# пишу, но если много печатать в консоль в цикле for, то это очень тормозит, и смысла как-то извращаться нету.
                                                                                                                      Либо уменьшать кол-во сообщений, либо писать всё в буфер и печатать в конце выполнения, но так, не будет видно на каком этапе идёт выполнение программы.
                                                                                                                      Самый просто способ это печатать буфер скажем каждые 10,20, 100 сообщений.
                                                                                                                      операции логики можно сказать ничего не стоят, когда как операция печати очень даже тормозять
                                                                                                                        +2
                                                                                                                        Это называется «буферизация».
                                                                                                                        В стандартной библиотеке C (на Linux, по крайней мере) когда вывод идет на консоль, стандартные функции вывода (f)printf/(f)puts/fwrite/etc используют line buffering, т.е. каждый символ `\n` приводит к fflush'у. Когда вывод перенаправлен в файл, то по умолчанию full buffering с буфером размера BUFSIZ, но при желании можно установить и свой буфер, побольше.
                                                                                                                        Смысл в том, что, как я написал в начале, весь вывод идет в файл или даже в /dev/null, так что буферизация там уже есть.
                                                                                                                        0
                                                                                                                        Мне, как человеку, в жизни не написавшему ни строчки кода, любопытно, на сколько быстрее будет код, не проверяющий делимость числа на 3 или 5 напрямую? Из школького курса математики я помню, что:
                                                                                                                        * число делится на 5, если его последняя цифра — 5 или 0
                                                                                                                        * число делится на 3, если сумма его цифр делится на 3
                                                                                                                          +1
                                                                                                                          ЦПУ работают в двоичном коде, а е в 10м, так что игрища быстрым определением делимости на 2, 5 и 10 работают для только для 2 и его степеней и активно используются в современных программах.
                                                                                                                          Операции деления-умножения на современных ЦПУ для чисел до 64 бит(а иногда и более) — это 1 такт, так что косвенные признаки делимости(на 3 в вашем примере) начинают иметь смысл на ОЧЕНЬ больших числах — грубо тысячебитных и более.
                                                                                                                          чтобы уж совсем стало: ясно беззнаковое 64 бит число — это от 0 до 18,446,744,073,709,551,615
                                                                                                                            +2

                                                                                                                            Intel optimization guide 2020 говорит нам на странице 758, что DIVPD(xmm) занимает 14–20 тактов, а MULPD(xmm) — 3–5 тактов. Тут, конечно, деление на константу, которое компилятор превратит в умножение, но всё ещё не один такт. На странице 762 ниже есть числа для некоторых невекторных инструкций, и там тоже не 1 такт.


                                                                                                                            С остальным, конечно, не поспоришь. Переводить в base10 и использовать признаки делимости из неё никакого смысла.

                                                                                                                              0
                                                                                                                              Ну на счет 1 такта я может и приврал, но DIVPD это все-таки другой зверь. Это SIMD double-precision float деление, а мы обсуждаем целочисленное обычное 64 деление, т.е. DIV. Сорян, но мне очень лень искать тайминги этой инструкции. Да и смысла нет. Реальное время исполнения может варьироваться на пару порядков в зав-сти от состояния кэшей, загруженности параллельных блоков АЛУ, от того, что там аппаратному оптимизатору пригрезилось в коде и т.д. Может и 0 тактов оказаться, если сработало суперскалярно в параллель с другой инструкцией.
                                                                                                                              Для примера — 32бит деление(DIV/UDIV) на весьма тупеньком по нонешним временам ARM Cortex M4 — от 2 до 12 тактов.
                                                                                                                                0

                                                                                                                                Я векторную привёл, потому что для скалярных там пустые клеточки с примечанием, что latency ≈ throughput, как будто нет конвейера. И там div r64 — 80–95, div r32 — 20–26.


                                                                                                                                Но опять же, поскольку мы делим на константу, то это умножение, т.е. mul r64 с задержкой 3–5 тактов и throughput 1, что уже таки да, близко к 1 такту на операцию, если они хорошо переупорядочатся.

                                                                                                                            0
                                                                                                                            Это признаки делимости в десятичной системе счисления. Переводить двоичную запись в десятичную слишком долго.
                                                                                                                            –1
                                                                                                                            По-синиорски — это ещё учесть что клиент забыл сказать что если число кратно 7 то нужно писать «Tazz» или что-то в этом роде. Тогда почти все варианты придётся с нуля переписывать.
                                                                                                                              +2
                                                                                                                              Не с нуля, конечно, но изменения будут заметные, поскольку наименьшее общее кратное становится 105. Но это нормально — менять код, когда меняются требования.

                                                                                                                              Дело в том, что заранее никогда не знаешь, как они изменятся, и, как показывает опыт, если ты закладываешься на некое возможное изменение какого-то требования. меняется какое-то другое требование, к которому ты не готов. Такая разновидность закона Мэрфи для программистов.
                                                                                                                                0
                                                                                                                                Понятно что всё учесть нельзя, но самые вероятные изменения в данном случае это
                                                                                                                                — заменить %3 или %5 на что-то другое
                                                                                                                                — убрать один из них
                                                                                                                                — добавить ещё один %N
                                                                                                                                — поменять надписи «Fizz» или «Buzz» на что-то другое
                                                                                                                                Чтобы сделать одно из первых изменений во многих примерах придётся много переписывать.
                                                                                                                              0

                                                                                                                              Все неподдерживаемые приседания с шага 2 до многопоточности ускорили код в 4 с небольшим раза. Даже с первого наивного варианта — разница в восемь раз. Современные машины, на которых будет выполняться такой код — уж как-нибудь наделены 4 камнями с гипертредингом.


                                                                                                                              Если в XXI веке человек начинает с микрооптимизации условных переходов и ручной буферизации, его можно смело отправлять обратно в 1980-й год. Я — от действительно профессионала — ждал бы только многопоточность (и общие слова про низкоуровневую шлифовку надфилем, с комментарием, почему она здесь и сейчас — не только не нужна, но даже вредна).

                                                                                                                                +3
                                                                                                                                > Все неподдерживаемые приседания с шага 2 до многопоточности ускорили код в 4 с небольшим раза. Даже с первого наивного варианта — разница в восемь раз. Современные машины, на которых будет выполняться такой код — уж как-нибудь наделены 4 камнями с гипертредингом.

                                                                                                                                Что значит «неподдерживаемые»? Где? Если на ARMе или до-Haswell'овских Intel'ах (не помню AMD'шную линейку), то да, но герой оптимизировал под конкретный проц в своем конкретном лаптопе. И если будет задача оптимизации какого-то сервиса, например, то это нормально — учитывать, на какой архитектуре этот сервис бежит. Можно было реализовать как простое побайтовое сравнение, но зачем, если есть векторные инструкции? Тем более герой хотел уесть интервьюера, у него была мотивация.
                                                                                                                                И ускорение чего-то в 4 и 8 раз — это офигенно хороший результат. Я, бывает, на работе бьюсь неделями, чтобы выжать какие-то 10% из сервиса.

                                                                                                                                > Если в XXI веке человек начинает с микрооптимизации условных переходов и ручной буферизации, его можно смело отправлять обратно в 1980-й год
                                                                                                                                Одним махом уменьшить кол-во branch инструкций в 45 раз, и количество вызовов printf'а в 15 раз, что дало ускорение почти в 2 раза — микрооптимизация? Ну ok. Готов к ссылке в 1980, я тогда был сильно моложе :)
                                                                                                                                  +2
                                                                                                                                  Посмотрел бы я на проект пяти таких «синьоров», каждый из которых для создал бы себе по 8 потоков для своей задачи…
                                                                                                                                    0

                                                                                                                                    Правильно ли я понимаю, что все программы которым нужны оптимизации уже написали в 1980м и новые не нужны?

                                                                                                                                    +7
                                                                                                                                    На самом деле статья была о том, что «в жизни всегда есть место подвигу», т.е. почти в любой задаче найдется, что пооптимизировать, было бы желание, а вся история про якобы интервью — для драмы.

                                                                                                                                    Но я удивился, увидев большое кол-во комментов о том, что не сениорское это дело — такие оптимизации. Мне кажется, есть два взгляда на то, кто такой сениор:
                                                                                                                                    1. Сениор знает методики, пишет простой код с низкой цикломатической сложностью, этот код легко читать и поддерживать. Работает в большой конторе с четкими стандартами, читает на досуге GoF и «чистые» книги от дяди Боба. От джуниора отличается тем, что глубже влезает в предметную область, пишет код, который потом проще развивать. Он знает, где грабли, и обходит их метров за 10, так что нос у него красивый и ровный.
                                                                                                                                    2. Сениор — опытный разработчик, который представляет, что с его кодом сделает компилятор, как он будет исполняться, знает, почему в большинстве случаев массив намного лучше связанного списка, при необходимости умеет в алгоритмы. Работает скорее всего в «бутике». Читает разное — МакКоннела, Ричарда Стивенса, да всякое разное, ну может кроме дяди Боба. От джуниора отличается тем, что занимается самыми сложными и нестандартными задачами, вянет от рутины. Про цикломатическую сложность его лучше не спрашивать, если не хочешь быть посланным куда подальше. Он тоже знает, где грабли, и ловко лавирует между ними, ну иногда получает по носу, как без этого.

                                                                                                                                    Речь не о том, какой взгляд более правильный — скорее всего они оба правильные, просто каждый для своей ситуации. В «кровавом энтырпрайзе» от второго типа будет больше проблем, зато для «бутика» это находка. А когда первый тип приходит в «бутик» и пытается там навести порядок, редко это дает хороший результат, а в большой конторе он — столп. Бытие определяет сознание.

                                                                                                                                    Мне лично ближе второй вариант, и примерно про такого сениора и был мой рассказ.
                                                                                                                                      0

                                                                                                                                      Лично знаю людей совмещающих оба пункта. Вот уж кого можно смело записывать в сениоры. ИМХО оба навыка важны, как и хард/софтскиллы, например. Знание как спроектировать архитектуру на высоком уровне не умаляет желания поковыряться в "байтиках" с vtune

                                                                                                                                      0
                                                                                                                                      Если к объему кода требований нет, можно 2й пример с увеличением строки довести до без циклового. Миллиард строк, а Printf потянет?
                                                                                                                                      Ведь 2й пример дал увеличение производительности в 2 раза при минимуме затрат синьерства.
                                                                                                                                        0
                                                                                                                                        У меня была мысль попробовать блоками не по 15, а, скажем, по 45, чтобы оценить, как это влияет, но было лень столько кода писать :)

                                                                                                                                        Для миллиарда уже было бы проще вообще не дергать printf, а просто подготовить сразу буфер с результатом, и его вывести — это уже выше предлагали. Размер программы вырастает на 7.5 Gb. Кстати, подозреваю, что станет сильно медленнее, так как при чтении .data (через memory-mapped I/O) опять же упираемся в кучу page fault'ов, значит переключения user space/kernel space, а это медленно.
                                                                                                                                          0
                                                                                                                                          Кстати, подозреваю, что станет сильно медленнее, так как при чтении .data (через memory-mapped I/O) опять же упираемся в кучу page fault'ов, значит переключения user space/kernel space, а это медленно.
                                                                                                                                          А время загрузки программы засчитывается к времени выполнения? А если с рамдиска запускать, то что будет?
                                                                                                                                            0
                                                                                                                                            > А время загрузки программы засчитывается к времени выполнения?

                                                                                                                                            Если засекать время, используя time (я именно так делал), то да, будет. Но вся программа не будет сразу грузится в этом случае, Linux мапит куски исполняемого файла в память, отдельно по сегментам, и в данном случае .data будет отдельно замаплен, и будет подгружаться по мере необходимости (ну как page fault случится), при этом будут использоваться скорее всего стандартные 4K-страницы, так что не исключаю, что каждый раз будет читаться с диска 4K, и это будет страшно медленно. Но я все же надеюсь, что тут есть какие-то оптимизации, и можно страницы читать пачками.

                                                                                                                                            Запуск с RAM-диска, конечно, ускорит чтение, но от page fault'ов никуда не уйти, а это опять переключение user space/kernel space, а с недавних пор, спасибо Meltdown и Spectre, это стало очень дорогой операцией.
                                                                                                                                              0
                                                                                                                                              Хм, а если программой просто создать файл с айнодами, направленными на блоки, относящиеся к той части файла программы, в которых лежит .data? Или вообще пусть просто обрезает от собственного исполняемого файла исполняемую часть?
                                                                                                                                                0
                                                                                                                                                Так можно просто заранее положить файл и переименовать, или линк на него сделать :)
                                                                                                                                                  0
                                                                                                                                                  Ну, это уже неспортивно, так можно и без программы обойтись. Просто положить файл, и сказать, что программа не требует запуска.
                                                                                                                                                    +1
                                                                                                                                                    Ну очень тонкая грань :)
                                                                                                                                                      0
                                                                                                                                                      Когда предложили концепцию сформировать данные в compile-time, я подумал, что это действие можно включить в makefile.
                                                                                                                                          0
                                                                                                                                          Синьор почти уже успокоился и вышел из переговорки, но тут HR обозвал его джуном…
                                                                                                                                            0
                                                                                                                                            Боюсь, в этом случае мы бы узнали, как писать FizzBuzz для кластера :)
                                                                                                                                              +5
                                                                                                                                              Как сумасшедший ИИ изводит планету на скрепки, так взбешённый синьор запускает параллельный FizzBuzz на всех доступных машинах, до которых он может дотянуться.
                                                                                                                                                0
                                                                                                                                                Первый обитатель матрицы, просто газонокосильщик, именно так и делал. Видимо, с тех пор повелось.
                                                                                                                                                +1
                                                                                                                                                Ждал реализации на VHDL.

                                                                                                                                            Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                                                                                                                            Самое читаемое