如何区分本质和偶然的复杂性

如果您想找一个简单的答案,就在这里。 如果这让您感到恼火,或者您想知道为什么这件事是如此复杂,那是偶然的。 如果这使您感到沮丧,那么如果您确切地知道为什么会这样,那就至关重要。

如果您需要更详尽的解释,那么恐怕对您来说是个坏消息。 您深入研究主题,界限变得越微妙。

这些术语来自Fred Brooks著名的“ No Silver Bullet-软件工程的本质和事故 ”论文。 本文的实质可以用单引号引起来:

无论是技术还是管理技术,都没有单一的发展,其本身有望在十年内提高生产率,可靠性和简便性,甚至可以提高一个数量级 [十倍]。

这里偶然的复杂性只是一种解释工具,高级语言如何将生产力提高了十倍的发展。 就像生活中所有美好的事物一样,可以通过分解来说明。

考虑一个简单的问题:

假设所有变量都是非负整数。 在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);
}

现在是有趣的部分。 如果bc都大于0 ,则它们的和不能小于bc 。 这意味着大分母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

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
【为什么学习这门课程?】 课程教授如何通过模式、面向对象设计技术和Java编程语言的特性来开发高质量并发软件应用程序和可重用框架。 多核的分布式核处理器、廉价的大容量存储、无处不在的连接性和通用软件平台的融合趋势,正推动着软件工程师和程序员的需求变化,他们需要知道如何为连接到云计算平台的客户端设备开发并发软件。尽管目前在处理器、存储和网络方面有许多改进,但是从客观上说,想要根据预算额度按时开发和交付高质量的软件仍然是有难度的,特别是开发高质量的并发软件应用程序和可重用服务。 【课程亮点】 本课程通过示例描述了如何通过使用面向对象的设计技术、Java编程语言特性、类库、应用模式和框架等技术要点,来有效降低并发软件开发的复杂性。课程中使用了许多Java应用程序示例来展示并发软件中的面向模式设计和编程技术。 【讲师介绍】 Douglas C. Schmidt(道格拉斯·施密特)—— ACE / TAO初始研发者、《C++网络编程》作者 施密特博士是ACE、TAO和CIAO的初始研发者,过去的20年里,领导了面向模式DRE中间件框架的发展。这些技术已被全球数千家公司和机构成功应用于许多领域,包括国防和安全、数据通信/电信、金融服务、医疗工程和大型多人在线游戏。施密特博士曾担任卡耐基梅隆大学软件工程学院的首席技术官,目前是美国范德堡大学(Vanderbilt University)的计算机科学教授。主要研究分布式实时和嵌入式系统的模式、优化、中间件和基于模型的工具。  施密特博士还是《C++报告》的前主编和《C/ C++用户》杂志专栏作家。发表了500多篇技术论文相关的话题, 主要涉及模式、优化技术、面向对象的框架和实证分析和特定领域的建模环境。与人合著了四本模式领域的经典书,包括《C++网络编程》第一卷和第二卷、《编程设计中的模式语言》、《面向模式的软件设计》。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值