如果您想找一个简单的答案,就在这里。 如果这让您感到恼火,或者您想知道为什么这件事是如此复杂,那是偶然的。 如果这使您感到沮丧,那么如果您确切地知道为什么会这样,那就至关重要。
如果您需要更详尽的解释,那么恐怕对您来说是个坏消息。 您深入研究主题,界限变得越微妙。
这些术语来自Fred Brooks著名的“ No Silver Bullet-软件工程的本质和事故 ”论文。 本文的实质可以用单引号引起来:
无论是技术还是管理技术,都没有单一的发展,其本身有望在十年内提高生产率,可靠性和简便性,甚至可以提高一个数量级 [十倍]。
这里偶然的复杂性只是一种解释工具,高级语言是如何将生产力提高了十倍的发展。 就像生活中所有美好的事物一样,可以通过分解来说明。
考虑一个简单的问题:
![](https://i-blog.csdnimg.cn/blog_migrate/a380b44da8e4b45724c24fc767d80f5f.gif)
假设所有变量都是非负整数。 在x64 GCC反汇编中,执行计算的函数如下所示。
_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);
}
本质上,这是相同的代码,但几乎只有一半。 它也明显更简单。 那么删除代码是偶然的吗? 我们确定不是。 我们如何安全地将其删除?
现在拆卸必不可少了吗? 反正不适合我们。 那么,为什么我们引入了更多的代码来简化它呢?
您会看到,当您深入研究复杂性隔离的业务时,问题多于答案。 我认为,基本上所有复杂性都是偶然的。 所有的硬件,所有的问题和所有的语言都是人发明的。 这都是复杂的坚实世界。 有时,通过为任务选择正确的语言,可以使程序员的任务变得简单两倍。 有时,您可以通过明确该任务不值得解决来使其变得绝对简单。
我们所有人都生活在庞大的复杂网络中。 您可以通过制造自己的硬件,或者通过解决自己的问题来建立自己的业务来达到自己的目标。 它根本没有以编译器或软件结尾。 我们仅将视线限制在我们自己的责任范围内,不要疯狂。
但这不过是我个人的看法。 这就是我在职责范围内所做的事情。 我有意见。 在此过程中,我可能会失去一些理智。
From: https://hackernoon.com/how-to-tell-essential-and-accidental-complexity-apart-402918a7aa13