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

Если вы ищете более подробное объяснение, боюсь, у меня для вас плохие новости. Чем глубже вы вникаете в предмет, тем тоньше становятся границы.

Эти термины взяты из знаменитой статьи Фреда Брукса Нет серебряной пули - сущность и случайность в разработке программного обеспечения. Суть статьи можно выразить одной цитатой:

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

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

Рассмотрим простую задачу:

Допустим, все переменные - неотрицательные целые числа. В дизассемблировании GCC x64 функция, выполняющая вычисления, выглядит так.

_Z12do_the_mathsmmm:
.LFB1426:
 .cfi_startproc
 pushq %rbp
 .cfi_def_cfa_offset 16
 .cfi_offset 6, -16
 movq %rsp, %rbp
 .cfi_def_cfa_register 6
 movq %rdi, -8(%rbp)
 movq %rsi, -16(%rbp)
 movq %rdx, -24(%rbp)
 movq -16(%rbp), %rdx
 movq -24(%rbp), %rax
 addq %rdx, %rax        ; addition is essential
 movl $0, %edx
 divq -16(%rbp)         ; division is essential
 movq %rax, %rcx
 movq -16(%rbp), %rdx
 movq -24(%rbp), %rax
 addq %rdx, %rax        ; addition is essential
 movl $0, %edx
 divq -24(%rbp)         ; division is essential
 leaq (%rcx,%rax), %rsi
 movq -8(%rbp), %rax
 movl $0, %edx
 divq %rsi              ; division is essential
 popq %rbp
 .cfi_def_cfa 7, 8
 ret
 .cfi_endproc

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

В этом конкретном фрагменте соотношение случайных строк к существенным составляет примерно 4: 1. Давайте теперь посмотрим на исходный код.

uint64_t do_the_maths(uint64_t a, uint64_t b, uint64_t c)
{
   return a / ((b + c) / b + (b + c) / c);
}

Это всего лишь 2 значимые строки. Да, похоже, у него есть дополнительное дополнение, так что это не совсем оптимальное решение, но на самом деле здесь происходит то, что мы делегируем работу по предварительному вычислению b+c компилятору, и это делает все это компактным и эффективным.

В чистых строках код 2/25 близок к вышеупомянутому порядку величины. Мы устранили случайную сложность нашего кода, делегировав утомительную работу по оптимизации компилятору.

Но подожди минутку! Является ли ассемблерный код единственным источником случайной сложности? Конечно, нет. Поскольку написание ассемблера теперь является привилегией машины, мы изобрели целый новый мир высокого уровня сложности, с которым мы могли поиграть.

Возможно, это не было очевидным в 1987 году, когда была опубликована оригинальная статья, но большая часть случайных сложностей в настоящее время порождается попытками избежать сложности. Это просто замкнутый круг. На самом деле даже больше; это похоже на развивающуюся спираль. Вы решаете одну проблему - вот еще две.

Допустим, мы хотим, чтобы наш код работал как с целыми числами без знака, так и с числами с плавающей запятой без знака. Без проблем.

uint64_t do_the_maths(uint64_t a, uint64_t b, uint64_t c)
{
        return a / ((b + c) / b + (b + c) / c);
}
float do_the_maths(float a, float b, float c)
{
        return a / ((b + c) / b + (b + c) / c);
}

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

template <typename T>
T do_the_maths(T a, T b, T c)
{
        return a / ((b + c) / b + (b + c) / c);
}

Теперь он может работать с целыми числами, числами с плавающей запятой и всем, что имеет + и /. Бьюсь об заклад, это сработает даже на boost::path.

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

template <typename T>
T do_the_maths(T a, T b, T c)
{
        assert(a >= 0);
        assert(b >= 0);
        assert(c >= 0);
        return a / ((b + c) / b + (b + c) / c);
}

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

okaleniuk@bb:~/complexity$ ./main 
Floating point exception (core dumped)

Но для чисел с плавающей запятой он просто не работает тихо и может вызвать неприятные проблемы в другом месте. Мы не должны допустить этого!

template <typename T>
T do_the_maths(T a, T b, T c)
{
        assert(a >= 0);
        assert(b >= 0);
        assert(c >= 0);
        assert(b != 0);
        assert(c != 0);
        T d = ((b + c) / b + (b + c) / c);
        assert(d != 0);
        return a / d;
}

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

_Z12do_the_mathsIiET_S0_S0_S0_:
.LFB1540:
 .cfi_startproc
 pushq %rbp
 .cfi_def_cfa_offset 16
 .cfi_offset 6, -16
 movq %rsp, %rbp
 .cfi_def_cfa_register 6
 subq $32, %rsp
 movl %edi, -20(%rbp)
 movl %esi, -24(%rbp)
 movl %edx, -28(%rbp)
 cmpl $0, -20(%rbp)
 jns .L4
 movl $_ZZ12do_the_mathsIiET_S0_S0_S0_E19__PRETTY_FUNCTION__, %ecx
 movl $7, %edx
 movl $.LC0, %esi
 movl $.LC1, %edi
 call __assert_fail
.L4:
 cmpl $0, -24(%rbp)
 jns .L5
 movl $_ZZ12do_the_mathsIiET_S0_S0_S0_E19__PRETTY_FUNCTION__, %ecx
 movl $8, %edx
 movl $.LC0, %esi
 movl $.LC2, %edi
 call __assert_fail
.L5:
 cmpl $0, -28(%rbp)
 jns .L6
 movl $_ZZ12do_the_mathsIiET_S0_S0_S0_E19__PRETTY_FUNCTION__, %ecx
 movl $9, %edx
 movl $.LC0, %esi
 movl $.LC3, %edi
 call __assert_fail
.L6:
 cmpl $0, -24(%rbp)
 jne .L7
 movl $_ZZ12do_the_mathsIiET_S0_S0_S0_E19__PRETTY_FUNCTION__, %ecx
 movl $10, %edx
 movl $.LC0, %esi
 movl $.LC4, %edi
 call __assert_fail
.L7:
 cmpl $0, -28(%rbp)
 jne .L8
 movl $_ZZ12do_the_mathsIiET_S0_S0_S0_E19__PRETTY_FUNCTION__, %ecx
 movl $11, %edx
 movl $.LC0, %esi
 movl $.LC5, %edi
 call __assert_fail
.L8:
 movl -24(%rbp), %edx
 movl -28(%rbp), %eax
 addl %edx, %eax
 cltd
 idivl -24(%rbp)
 movl %eax, %ecx
 movl -24(%rbp), %edx
 movl -28(%rbp), %eax
 addl %edx, %eax
 cltd
 idivl -28(%rbp)
 addl %ecx, %eax
 movl %eax, -4(%rbp)
 cmpl $0, -4(%rbp)
 jne .L9
 movl $_ZZ12do_the_mathsIiET_S0_S0_S0_E19__PRETTY_FUNCTION__, %ecx
 movl $13, %edx
 movl $.LC0, %esi
 movl $.LC6, %edi
 call __assert_fail
.L9:
 movl -20(%rbp), %eax
 cltd
 idivl -4(%rbp)
 leave
 .cfi_def_cfa 7, 8
 ret
 .cfi_endproc

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

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

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

template <typename T>
T do_the_maths(T a, T b, T c)
{
        assert(a >= 0);
        assert(b >= 0);
        assert(c >= 0);
        assert(b != 0);
        assert(c != 0);
        T d = ((b + c) / b + (b + c) / c);
        assert(d != 0);
        return a / d;
}
template <>
uint64_t do_the_maths(uint64_t a, uint64_t b, uint64_t c)
{
        return a / ((b + c) / b + (b + c) / c);
}

Итак ... это все важно, верно? Либо по совместимости, либо по производительности. Не менее 13 строк содержательного кода.

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

float do_the_maths(float a, float b, float c)
{
        assert(a >= 0);
        assert(b >= 0);
        assert(c >= 0);
        assert(b != 0);
        assert(c != 0);
        T d = ((b + c) / b + (b + c) / c);
        assert(d != 0);
        return a / d;
}
uint64_t do_the_maths(uint64_t a, uint64_t b, uint64_t c)
{
        return a / ((b + c) / b + (b + c) / c);
}

Вы можете возразить, что нам может потребоваться другой тип достаточно скоро, и с параметрическим решением у нас был бы новый экземпляр бесплатно. Но нет ничего бесплатного. Мы получили массу неудач от простого поплавка; мы не можем надеяться, что любой другой тип легко интегрируется. Более вероятно, что это принесет целый новый спектр трудно обнаруживаемых проблем. В определенных случаях нам придется столкнуться с ними заранее, и это хорошо.

Теперь очевидно, что нам не нужны отдельные строки для неотрицательности и не нуля, мы можем легко соединить их, то есть еще две строки.

float do_the_maths(float a, float b, float c)
{
        assert(a >= 0);
        assert(b > 0);
        assert(c > 0);
        T d = ((b + c) / b + (b + c) / c);
        assert(d != 0);
        return a / d;
}
uint64_t do_the_maths(uint64_t a, uint64_t b, uint64_t c)
{
        return a / ((b + c) / b + (b + c) / c);
}

Теперь самое интересное. Если оба b и c больше, чем 0, их сумма не может быть меньше, чем b или c друг от друга. Это означает, что обе части большого знаменателя d гарантированно будут 1 или больше. Это делает последнее утверждение избыточным, и нам не нужно вводить новую переменную для d. Это еще две строчки.

float do_the_maths(float a, float b, float c)
{
        assert(a >= 0);
        assert(b > 0);
        assert(c > 0);
        return a / ((b + c) / b + (b + c) / c);
}
uint64_t do_the_maths(uint64_t a, uint64_t b, uint64_t c)
{
        return a / ((b + c) / b + (b + c) / c);
}

По сути, это тот же код, но он почти вдвое меньше. Это также заметно проще. Так был ли код удален случайно? Мы вроде как установили, что это не так. Как получилось, что мы удалили его безопасно?

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

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

Мы все живем в огромной сложной сети. Вы можете пойти сколько угодно, создавая собственное оборудование или создавая собственный бизнес, решая собственные проблемы. Это вовсе не заканчивается компилятором или программным обеспечением. Мы ограничиваем наш кругозор только нашей собственной сферой ответственности, чтобы не сойти с ума.

Но это не более чем мое собственное мнение. Именно этим я и занимаюсь в сфере своей ответственности. Я делаю мнения. Возможно, в процессе я немного теряю рассудок.