我最近在亚马逊接受了采访。在一次编码会议中,访调员问为什么我在方法中声明了变量。我解释了我的过程,他挑战我以更少的变量解决相同的问题。例如(不是来自采访),我从方法A开始,然后通过删除int s将其改进为方法B。他很高兴,并说这将通过这种方法减少内存使用。

我理解其背后的逻辑,但是我的问题是:

何时才适合使用方法A与方法B相对,反之亦然?

您可以看到,由于声明了int s,因此方法A将具有更高的内存使用率,但它只需要执行一个计算,即a + b。另一方面,方法B的内存使用量较低,但必须执行两次计算,即两次a + b。什么时候比另一种使用一种技术?还是其中一种技术总是总是比其他技术更受青睐?评估这两种方法时需要考虑什么?

方法A:



 private bool IsSumInRange(int a, int b)
{
    int s = a + b;

    if (s > 1000 || s < -1000) return false;
    else return true;
}
 


方法B:

 private bool IsSumInRange(int a, int b)
{
    if (a + b > 1000 || a + b < -1000) return false;
    else return true;
}
 


评论

我敢打赌,现代编译器会为这两种情况生成相同的程序集。

由于您的修改使我的答案无效,因此我将问题回滚到了原始状态-请不要这样做!如果您提出问题来改进代码,则不要通过以所示方式改进代码来更改问题-这会使答案显得毫无意义。

等待一秒钟,他们要求摆脱int,同时对上下限的那些魔术数字完全满意吗?

切记:优化之前先进行配置。使用现代编译器,可以将方法A和方法B优化为相同的代码(使用更高的优化级别)。同样,对于现代处理器,它们可能具有指令,这些指令在单个操作中执行的操作不仅仅限于加法运算。

都不;优化可读性。

#1 楼

不用猜测可能发生或可能发生的事情,让我们看看吧?我将不得不使用C ++,因为我没有方便的C#编译器(尽管请参阅VisualMelon的C#示例),但是无论如何我都相信相同的原理。
您在面试中遇到的两种选择。我们还将提供一些答案建议使用abs的版本。

 #include <cstdlib>

bool IsSumInRangeWithVar(int a, int b)
{
    int s = a + b;

    if (s > 1000 || s < -1000) return false;
    else return true;
}

bool IsSumInRangeWithoutVar(int a, int b)
{
    if (a + b > 1000 || a + b < -1000) return false;
    else return true;
}

bool IsSumInRangeSuperOptimized(int a, int b) {
    return (abs(a + b) < 1000);
}
 


现在,无需进行任何优化即可编译它:g++ -c -o test.o test.cpp

现在我们可以准确地看到生成的内容: objdump -d test.o在堆栈上使用了16个额外的字节(例如,-0x4中的mov %edi,-0x4(%rbp)-0x14中的mov %edi,-0x14(%rbp))的堆栈地址。

因为IsSumInRangeWithVar()在堆栈上没有分配空间来存储中间值IsSumInRangeWithoutVar()必须重新计算它,导致此实现的执行时间增加了2条指令。

有趣的是,s看起来与IsSumInRangeSuperOptimized()非常相似,不同的是它首先是-1000,然后是1000第二。

现在让我们仅使用最基本的优化进行编译:IsSumInRangeWithoutVar()。结果:

0000000000000000 <_Z19IsSumInRangeWithVarii>:
   0:   55                      push   %rbp              # begin a call frame
   1:   48 89 e5                mov    %rsp,%rbp
   4:   89 7d ec                mov    %edi,-0x14(%rbp)  # save first argument (a) on stack
   7:   89 75 e8                mov    %esi,-0x18(%rbp)  # save b on stack
   a:   8b 55 ec                mov    -0x14(%rbp),%edx  # load a and b into edx
   d:   8b 45 e8                mov    -0x18(%rbp),%eax  # load b into eax
  10:   01 d0                   add    %edx,%eax         # add a and b
  12:   89 45 fc                mov    %eax,-0x4(%rbp)   # save result as s on stack
  15:   81 7d fc e8 03 00 00    cmpl   
0000000000000000 <_Z19IsSumInRangeWithVarii>:
   0:   8d 84 37 e8 03 00 00    lea    0x3e8(%rdi,%rsi,1),%eax
   7:   3d d0 07 00 00          cmp    
IsSumInRangeWithVar
  2           0 LOAD_FAST                0 (a)
              3 LOAD_FAST                1 (b)
              6 BINARY_ADD
              7 STORE_FAST               2 (s)

  3          10 LOAD_FAST                2 (s)
             13 LOAD_CONST               1 (1000)
             16 COMPARE_OP               4 (>)
             19 POP_JUMP_IF_TRUE        34
             22 LOAD_FAST                2 (s)
             25 LOAD_CONST               4 (-1000)
             28 COMPARE_OP               0 (<)
             31 POP_JUMP_IF_FALSE       38

  4     >>   34 LOAD_CONST               2 (False)
             37 RETURN_VALUE

  6     >>   38 LOAD_CONST               3 (True)
             41 RETURN_VALUE
             42 LOAD_CONST               0 (None)
             45 RETURN_VALUE

IsSumInRangeWithoutVar
  9           0 LOAD_FAST                0 (a)
              3 LOAD_FAST                1 (b)
              6 BINARY_ADD
              7 LOAD_CONST               1 (1000)
             10 COMPARE_OP               4 (>)
             13 POP_JUMP_IF_TRUE        32
             16 LOAD_FAST                0 (a)
             19 LOAD_FAST                1 (b)
             22 BINARY_ADD
             23 LOAD_CONST               4 (-1000)
             26 COMPARE_OP               0 (<)
             29 POP_JUMP_IF_FALSE       36

 10     >>   32 LOAD_CONST               2 (False)
             35 RETURN_VALUE

 12     >>   36 LOAD_CONST               3 (True)
             39 RETURN_VALUE
             40 LOAD_CONST               0 (None)
             43 RETURN_VALUE

IsSumInRangeSuperOptimized
 15           0 LOAD_GLOBAL              0 (abs)
              3 LOAD_FAST                0 (a)
              6 LOAD_FAST                1 (b)
              9 BINARY_ADD
             10 CALL_FUNCTION            1 (1 positional, 0 keyword pair)
             13 LOAD_CONST               1 (1000)
             16 COMPARE_OP               1 (<=)
             19 RETURN_VALUE

Benchmarking
IsSumInRangeWithVar: 0.019361s
IsSumInRangeWithoutVar: 0.020917s
IsSumInRangeSuperOptimized: 0.020171s
x7d0,%eax c: 0f 96 c0 setbe %al f: c3 retq 0000000000000010 <_Z22IsSumInRangeWithoutVarii>: 10: 8d 84 37 e8 03 00 00 lea 0x3e8(%rdi,%rsi,1),%eax 17: 3d d0 07 00 00 cmp
IsSumInRangeWithVar: 0.000180s
IsSumInRangeWithoutVar: 0.001175s
IsSumInRangeSuperOptimized: 0.001306s
x7d0,%eax 1c: 0f 96 c0 setbe %al 1f: c3 retq 0000000000000020 <_Z26IsSumInRangeSuperOptimizedii>: 20: 8d 84 37 e8 03 00 00 lea 0x3e8(%rdi,%rsi,1),%eax 27: 3d d0 07 00 00 cmp
IsSumInRangeSuperOptimized: 0.000191s
IsSumInRangeWithoutVar: 0.001174s
IsSumInRangeWithVar: 0.001265s
x7d0,%eax 2c: 0f 96 c0 setbe %al 2f: c3 retq
x3e8,-0x4(%rbp) # compare s to 1000 1c: 7f 09 jg 27 # jump to 27 if it's greater 1e: 81 7d fc 18 fc ff ff cmpl q4312078qxfffffc18,-0x4(%rbp) # compare s to -1000 25: 7d 07 jge 2e # jump to 2e if it's greater or equal 27: b8 00 00 00 00 mov q4312078qx0,%eax # put 0 (false) in eax, which will be the return value 2c: eb 05 jmp 33 <_Z19IsSumInRangeWithVarii+0x33> 2e: b8 01 00 00 00 mov q4312078qx1,%eax # put 1 (true) in eax 33: 5d pop %rbp 34: c3 retq 0000000000000035 <_Z22IsSumInRangeWithoutVarii>: 35: 55 push %rbp 36: 48 89 e5 mov %rsp,%rbp 39: 89 7d fc mov %edi,-0x4(%rbp) 3c: 89 75 f8 mov %esi,-0x8(%rbp) 3f: 8b 55 fc mov -0x4(%rbp),%edx 42: 8b 45 f8 mov -0x8(%rbp),%eax # same as before 45: 01 d0 add %edx,%eax # note: unlike other implementation, result is not saved 47: 3d e8 03 00 00 cmp q4312078qx3e8,%eax # compare to 1000 4c: 7f 0f jg 5d <_Z22IsSumInRangeWithoutVarii+0x28> 4e: 8b 55 fc mov -0x4(%rbp),%edx # since s wasn't saved, load a and b from the stack again 51: 8b 45 f8 mov -0x8(%rbp),%eax 54: 01 d0 add %edx,%eax 56: 3d 18 fc ff ff cmp q4312078qxfffffc18,%eax # compare to -1000 5b: 7d 07 jge 64 <_Z22IsSumInRangeWithoutVarii+0x2f> 5d: b8 00 00 00 00 mov q4312078qx0,%eax 62: eb 05 jmp 69 <_Z22IsSumInRangeWithoutVarii+0x34> 64: b8 01 00 00 00 mov q4312078qx1,%eax 69: 5d pop %rbp 6a: c3 retq 000000000000006b <_Z26IsSumInRangeSuperOptimizedii>: 6b: 55 push %rbp 6c: 48 89 e5 mov %rsp,%rbp 6f: 89 7d fc mov %edi,-0x4(%rbp) 72: 89 75 f8 mov %esi,-0x8(%rbp) 75: 8b 55 fc mov -0x4(%rbp),%edx 78: 8b 45 f8 mov -0x8(%rbp),%eax 7b: 01 d0 add %edx,%eax 7d: 3d 18 fc ff ff cmp q4312078qxfffffc18,%eax 82: 7c 16 jl 9a <_Z26IsSumInRangeSuperOptimizedii+0x2f> 84: 8b 55 fc mov -0x4(%rbp),%edx 87: 8b 45 f8 mov -0x8(%rbp),%eax 8a: 01 d0 add %edx,%eax 8c: 3d e8 03 00 00 cmp q4312078qx3e8,%eax 91: 7f 07 jg 9a <_Z26IsSumInRangeSuperOptimizedii+0x2f> 93: b8 01 00 00 00 mov q4312078qx1,%eax 98: eb 05 jmp 9f <_Z26IsSumInRangeSuperOptimizedii+0x34> 9a: b8 00 00 00 00 mov q4312078qx0,%eax 9f: 5d pop %rbp a0: c3 retq


您会发现:每个变体都是相同的。编译器可以做一些非常聪明的事情:考虑到g++ -O1 -c -o test.o test.cpp进行无符号比较,abs(a + b) <= 1000等效于a + b + 1000 <= 2000,因此负数变为非常大的正数。 setbe指令实际上可以在一条指令中执行所有这些加法运算,并消除所有条件分支。

要回答您的问题,几乎总是要优化的不是内存或速度,而是可读性。读取代码要比编写代码难得多,而读取经过“优化”处理的代码要比读取清楚的代码要困难得多。通常,这些“优化”可以忽略不计,或者在这种情况下对性能的实际影响完全为零。用解释语言而不是编译语言?那么,优化很重要还是效果相同?


我们来衡量一下!我已将示例转录为Python:

 lea 


使用Python 3.5.2运行,产生输出: />
这三个功能的性能几乎相同。我们可能会喜欢def IsSumInRangeWithVar(a, b): s = a + b if s > 1000 or s < -1000: return False else: return True def IsSumInRangeWithoutVar(a, b): if a + b > 1000 or a + b < -1000: return False else: return True def IsSumInRangeSuperOptimized(a, b): return abs(a + b) <= 1000 from dis import dis print('IsSumInRangeWithVar') dis(IsSumInRangeWithVar) print('\nIsSumInRangeWithoutVar') dis(IsSumInRangeWithoutVar) print('\nIsSumInRangeSuperOptimized') dis(IsSumInRangeSuperOptimized) print('\nBenchmarking') import timeit print('IsSumInRangeWithVar: %fs' % (min(timeit.repeat(lambda: IsSumInRangeWithVar(42, 42), repeat=50, number=100000)),)) print('IsSumInRangeWithoutVar: %fs' % (min(timeit.repeat(lambda: IsSumInRangeWithoutVar(42, 42), repeat=50, number=100000)),)) print('IsSumInRangeSuperOptimized: %fs' % (min(timeit.repeat(lambda: IsSumInRangeSuperOptimized(42, 42), repeat=50, number=100000)),)) ,因为它的边际速度有所提高。尽管在尝试给IsSumInRangeWithVar()尝试不同的参数时会添加一些信息,但有时timeit的推出速度最快,因此我怀疑这可能是造成差异的外部因素,而不是任何实现方式的固有优势。如果这确实是对性能至关重要的代码,那么解释型语言就是一个非常糟糕的选择。使用pypy运行相同的程序,我得到:

q4312078q

仅使用pypy即可,它使用JIT编译消除了很多解释器的开销,从而提高了性能。 1或2个数量级。我很震惊地看到IsSumInRangeSuperOptimized()比其他的快一个数量级。因此,我更改了基准测试的顺序,然后再次运行:

q4312078q

因此,看来实现速度实际上并没有任何意义,而是执行基准测试的顺序!

我希望对此进行更深入的探讨,因为老实说我不不知道为什么会这样。但我相信观点已经提出:微观优化(例如是否将中间值声明为变量)很少相关。使用解释型语言或经过高度优化的编译器,首要目标仍然是编写清晰的代码。

如果可能需要进一步的优化,请进行基准测试。请记住,最佳的优化不是来自细节,而是来自更大的算法画面:对于相同功能的重复评估,pypy比cpython快一个数量级,因为它使用更快的算法(JIT编译器与解释)来评估程序。并且还需要考虑编码算法:通过B树进行搜索将比链接列表更快。

确保您使用了正确的工具和算法后,请做好准备。深入了解系统的细节。即使对于有经验的开发人员,结果也可能令人惊讶,这就是为什么您必须有一个基准来量化更改的原因。

评论


在C#中提供示例:SharpLab为这两种方法生成相同的asm(x86上的Desktop CLR v4.7.3130.00(clr.dll))

– VisualMelon
18-09-6在12:53

@VisualMelon足以进行肯定的检查:“返回((((a + b)> = -1000)&&((a + b)<= 1000));”给出了不同的结果。 :sharplab.io/…

– Pieter B
18年9月6日在13:57

可读性也可能使程序更易于优化。仅当编译器能够真正弄清楚您要做什么时,它才能轻松重写以使用与上面相同的逻辑。如果您使用许多老式的bithack,在int和指针之间来回转换,重用可变存储等,那么编译器很难证明变换是等效的,而这只会留下您编写的,可能不是最佳选择。

–卢申科
18/09/6在14:12



@Corey参见编辑。

–菲尔·弗罗斯特(Phil Frost)
'18 Sep 6'在14:56

@Corey:这个答案实际上是在告诉您我在答案中写的是什么:当您使用像样的编译器时,没有什么区别,而是专注于准备工作。当然,它看起来更好成立-也许您现在相信我。

–布朗博士
18年9月7日在5:39

#2 楼

要回答所述问题:

何时优化内存与方法的性能速度?

您必须建立两件事:

什么限制了您的应用程序?
我在哪里可以收回大部分资源?

为了回答第一个问题,您必须知道应用程序的性能要求是什么。如果没有性能要求,则没有理由优化一种方法或另一种方法。性能要求可帮助您达到“足够好”的位置。
您自己提供的方法本身不会导致任何性能问题,但可能会循环并处理大量数据,您必须开始对问题的处理方式有所不同。
检测限制应用程序的原因
开始使用性能监视器查看应用程序的行为。在运行时,请密切注意CPU,磁盘,网络和内存的使用情况。一个或多个项目将被用完,而其他所有东西都将被适当使用-除非您达到了完美的平衡,但这几乎永远不会发生。有内存分析器和进程分析器,它们测量的是不同的事物。性能分析确实会对性能产生重大影响,但是您正在检测代码以找出问题所在。
假设您看到CPU和磁盘使用率达到峰值。首先,您要检查“热点”或被调用次数比其他地方多或占处理时间比例明显更长的代码。记忆。也许您创建的对象超出了需要的数量,并且垃圾回收正在超时工作。
恢复性能
认真思考。以下更改列表是您将获得多少投资回报的顺序:

体系结构:寻找通信瓶颈
算法:处理数据的方式可能需要更改
热点:最小化热点的呼叫频率可以产生很大的收益
微观优化:并不常见,但是有时候您确实需要考虑一些细微的调整(例如您提供的示例),尤其是当它是代码中的热点时。

在这种情况下,您必须应用科学方法。提出一个假设,进行更改,然后进行测试。如果您达到了绩效目标,那么您就完成了。如果不是,请转到列表中的下一个内容。老实说,这是尝试处理性能或内存问题的最后一步。方法A与方法B的影响会因语言和平台的不同而有所不同(在某些情况下)。
对于任何带有中途优化器的编译语言,生成的代码都具有类似的结构。但是,这些假设在没有优化器的专有和玩具语言中并不一定会成立。
确切地说,要产生更好的影响取决于sum是堆栈变量还是堆变量。这是语言实现的选择。例如,在C,C ++和Java中,默认情况下,像int这样的数字原语是堆栈变量。通过分配给堆栈变量,您的代码不会比使用完全内联代码具有更多的内存影响。首先向下或首先跨维度数组是依赖于平台的优化。它需要一些有关您所针对的芯片组如何最优化内存访问的知识。架构之间存在细微的差异。
最重要的是,优化是艺术与科学的结合。它需要一些批判性思考,以及在处理问题时的一定程度的灵活性。在指责小事情之前先寻找大事情。

评论


这个答案最着重于我的问题,不会引起我的编码示例(即方法A和方法B)的困扰。

– Corey P
'18 Sep 4'在23:05

我觉得这是“您如何解决性能瓶颈”的通用答案,但是您将很难根据使用此方法的特定函数(具有4个变量还是5个变量)从特定函数中识别相对内存使用情况。我还质疑,当编译器(或解释器)可能会或可能不会对此进行优化时,此优化级别的相关性如何。

–埃里克
'18 Sep 5'在0:24

正如我提到的,@ Eric,性能改进的最后一类将是您的微优化。判断是否会产生影响的唯一方法是通过在探查器中测量性能/内存。这些类型的改进很少能获得回报,但是在模拟器中对时序敏感的性能问题中,您会遇到一些妥善放置的更改,这些更改可能是达到时序目标与不达到时序目标之间的区别。我想我一方面可以指望在20多年的软件开发工作中获得回报的次数,但这不为零。

–贝琳·洛里奇(Berin Loritsch)
18/09/5'上午0:41

@BerinLoritsch再次,总的来说,我同意你的观点,但是在这种情况下,我不同意。我提供了自己的答案,但是我个人还没有看到任何工具可以标记或什至为您提供潜在识别与函数的堆栈内存大小相关的性能问题的方法。

–埃里克
'18 Sep 5'在1:09

@DocBrown,我已经补救了。关于第二个问题,我几乎同意你的看法。

–贝琳·洛里奇(Berin Loritsch)
18/09/5在12:50

#3 楼

“这会减少内存”-em,不。即使这是正确的(对于任何体面的编译器都不是),对于任何现实情况,这种差异很可能都可以忽略不计。略有变化的A):

private bool IsSumInRange(int a, int b)
{
    int sum = a + b;

    if (sum > 1000 || sum < -1000) return false;
    else return true;
    // (yes, the former statement could be cleaned up to
    // return abs(sum)<=1000;
    // but let's ignore this for a moment)
}


但由于两个完全不同的原因:


通过给变量s解释名称,代码变得更清晰
避免在代码中使用两次相同的求和逻辑,因此代码变得更干燥,这意味着更少的错误发生。


评论


我将进一步清理它并使用“ return sum> -1000 && sum <1000;”。

– 26之17
18年9月4日在18:28

@Corey任何体面的优化器都会将CPU寄存器用于sum变量,从而导致内存使用量为零。即使不是,这也只是“叶子”方法中的一个单词。考虑到由于它们的GC和对象模型而导致的Java或C#浪费内存的情况令人难以置信,因此本地int变量实际上不使用任何明显的内存。这是毫无意义的微观优化。

–阿蒙
18-09-4在18:42



@Corey:如果它“稍微复杂一点”,它可能不会变成“显着的内存使用情况”。也许如果您构建一个更复杂的示例,但这使问题变得不同。还要注意,只是因为您没有为表达式创建特定的变量,对于复杂的中间结果,运行时环境仍可能在内部创建临时对象,因此它完全取决于语言,环境,优化级别和不管你怎么称呼“值得注意”。

–布朗博士
18-09-4在19:33



除了以上几点,我很确定C#/ Java如何选择存储总和将是一个实现细节,并且我怀疑有人会说服像避免一个本地int这样的愚蠢技巧导致令人信服的情况吗?从长远来看,这种或那样的内存使用量。 IMO的可读性更为重要。可读性可能是主观的,但是FWIW个人而言,我不希望您两次执行相同的计算,而不是为了CPU使用率,而是因为我在寻找错误时只需要检查一次添加。

– jrh
18年9月4日在22:12

…还请注意,一般来说,垃圾收集的语言是一种不可预测的“搅动内存的海洋”(无论如何对于C#而言)只能在需要时进行清理,我记得编写了一个分配了GB的RAM的程序,并且该程序只能启动“当记忆变得稀缺时,便会自行清理”。如果GC不需要运行,则可能需要花费很多时间,并节省了CPU的处理时间。

– jrh
18-09-4在22:13



#4 楼

与使用
return (abs(a + b) > 1000);
的两个处理器相比,您可以做得更好。您不仅拥有更少的总和,而且拥有的比较数也更少,这通常在计算上更加昂贵。它还消除了分支,这在大多数处理器上更加糟糕,因为它停止了流水线化。

正如其他答案所说,访问员是工厂生命周期,没有进行技术采访的业务。 br />
话说,他的问题是有效的。关于何时进行优化以及如何进行优化的答案是,当您证明它是必需的时,并且已经对其进行了概要分析以证明确切的零件需要它。克努斯(Knuth)有句著名的话说,过早的优化是万恶之源,因为试图将不重要的部分镀金或进行无效的更改(例如面试官的更改)太容易了,而错过了真正需要它的地方太容易了。在没有确凿的证据证明这确实是必要的之前,代码的清晰是更重要的目标。

编辑FabioTurati正确地指出,这与原始逻辑相反(我的错误!),并且这说明了Knuth报价的进一步影响,在尝试优化代码时,我们冒着破坏代码的风险。

评论


@Corey,我非常确定Graham会按预期将请求“他挑战我以更少的变量来解决相同的问题”。如果我想成为面试官,我会期待得到答案,而不是将a + b插入if并执行两次。您理解错了“他很高兴并表示这将通过这种方法减少内存使用” –他对您很好,对这种关于内存的无意义的解释隐藏了他的失望。您不应该在这里认真提问。你找到工作了吗?我猜你没有:-(

– Sinatr
18年9月5日在13:30

您将同时应用2个转换:使用abs()将2个条件转换为1,并且还具有单个返回,而不是当条件为true时返回一个(“ if branch”),而另一个返回如果为假(“其他分支”)。当您以这种方式更改代码时,请当心:可能会无意中编写一个在应返回false时返回true的函数,反之亦然。这正是这里发生的情况。我知道您正在专注于另一件事,并且您在此方面做得很好。不过,这很容易使您付出工作的代价...

–法比奥说恢复莫妮卡
18-09-5在20:49

@FabioTurati很好-感谢!我将更新答案。这是关于重构和优化的一个好点,这使Knuth的报价更加相关。在冒险之前,我们应该证明需要优化。

–格雷厄姆
'18 Sep 6'在1:24

大多数处理器(以及因此的编译器)都可以在单个操作中执行abs()。不幸的是,整数不是这样。如果已经从add中设置了标志,则ARM64具有条件取反,可以使用它,并且ARM带有谓词reverse-sub(rsblt = reverse-sub,如果less-tha除外),但其他所有条件都需要多个额外的指令来实现abs(a + b)或abs(a)。 godbolt.org/z/Ok_Con显示了x86,ARM,AArch64,PowerPC,MIPS和RISC-V asm输出。只有将比较转换为范围检查(无符号)(a + b + 999)<= 1998U,gcc才能像Phil的回答中那样对它进行优化。

– Peter Cordes
18-09-6在21:15



此答案中的“改进”代码仍然是错误的,因为它为IsSumInRange(INT_MIN,0)生成了不同的答案。原始代码返回false,因为INT_MIN + 0> 1000 || INT_MIN + 0 <-1000;但是“新的和改进的”代码返回true,因为abs(INT_MIN + 0)<1000。(或者,在某些语言中,它将引发异常或具有未定义的行为。请检查您的本地列表。)

– Quuxplusone
18-09-7在3:36



#5 楼


什么时候应该使用方法A与方法B,反之亦然?


硬件便宜;程序员很昂贵。因此,你们两个人在这个问题上浪费的时间可能比任何一个答案都要糟得多。

不管怎样,大多数现代编译器都会找到一种方法来将局部变量优化到寄存器中(而不是分配堆栈空间),因此就可执行代码而言,这些方法可能完全相同。因此,大多数开发人员会选择最清楚地传达意图的选项(请参阅编写非常明显的代码(ROC))。在我看来,那应该是方法A。 >
private bool IsSumInRange(int a, int b)
{
    a += b;
    return (a >= -1000 && a <= 1000);
}


评论


a + = b是一个巧妙的技巧,但我不得不提一下(以防万一,答案的其余部分没有暗示),从我的经验中,弄乱参数的方法可能很难调试和维护。

– jrh
18-09-4在22:21



我同意@jrh。我是中华民国的坚决拥护者,而那只是一回事。

–吴宗宪
18年9月4日在22:40

硬件便宜;程序员昂贵。在消费电子世界中,该说法是错误的。如果您卖出数百万个产品,那么花费50万美元的额外开发成本来节省每单位硬件成本0.10美元是一个很好的投资。

–巴特·范·恩根·舍瑙(Bart van Ingen Schenau)
18/09/5在10:34

@JohnWu:您简化了if检查,但是忘记了反转比较结果;当a + b不在范围内时,您的函数现在返回true。要么加一个!到条件的外部(返回!(a> 1000 || a <-1000)),或分布!进行反向测试,以得到返回值<= 1000 && a> = -1000;为了使范围检查顺利进行,请返回-1000 <= a && a <= 1000;

–ShadowRanger
18-09-5在19:03



@JohnWu:仍处于边缘情况,分布式逻辑需要<= /> =而不是(使用,1000和-1000被视为超出范围,原始代码将其视为处于范围内) 。

–ShadowRanger
18/09/5在19:30



#6 楼

我会针对可读性进行优化。
方法X:

private bool IsSumInRange(int number1, int number2)
{
    return IsValueInRange(number1+number2, -1000, 1000);
}

private bool IsValueInRange(int Value, int Lowerbound, int Upperbound)
{
    return  (Value >= Lowerbound && Value <= Upperbound);
}


只做一件事情但很容易推理的小方法。 />(这是个人喜好,我喜欢正面测试而不是负面测试,您的原始代码实际上正在测试该值是否不在范围内。)

评论


这个。 (上面提到的评论与re:readability类似)。 30年前,当我们使用RAM小于1mb的机器时,必须压缩性能-就像y2k问题一样,获得数十万条记录,其中每条记录都有几字节的内存由于未使用的var和引用等,当您只有256k的RAM时,它很快就会加起来。现在,我们正在处理具有多个GB内存的机器,因此即使节省几MB的RAM使用量以及代码的可读性和可维护性也不是一件好事。

– ivanivan
18/09/5在13:19

@ivanivan:我不认为“ y2k问题”确实与内存有关。从数据输入的角度来看,输入两位数字要比输入四位数字更有效,并且将输入的内容保持为比将它们转换为其他形式更容易。

–超级猫
'18 Sep 5'在16:13

现在,您必须跟踪两个函数以查看发生了什么。您不能从表面上看待它,因为您不能从名称中分辨出它们是包含边界还是排除边界。而且,如果添加了该信息,则函数的名称将比表示它的代码长。

–彼得
'18 Sep 5'在16:27

优化可读性,并制作小巧,易于使用的功能-当然可以。但是我强烈不同意将a和b重命名为number1和number2会以任何方式提高可读性。同样,您对函数的命名也不一致:如果IsValueInRange接受范围作为参数,为什么IsSumInRange会硬编码范围?

–leftaround关于
18/09/5在19:30



第一个功能可能会溢出。 (就像其他答案的代码一样。)尽管溢出安全代码的复杂性是将其放入函数的一个论据。

–philipxy
18-09-5在23:36

#7 楼

简而言之,我认为这个问题与当前的计算没有太大关系,但是从历史的角度来看,这是一个有趣的思想练习。在本书中,弗雷德·布鲁克斯(Fred Brooks)提出了这样的假设:程序员通常会在其工具箱中需要两个版本的关键功能:内存优化版本和cpu优化版本。弗雷德(Fred)基于他领导IBM System / 360操作系统开发的经验,该系统中的机器可能只有8 KB的RAM。在这样的机器中,函数中局部变量所需的内存可能很重要,特别是如果编译器没有有效地优化它们(或者如果代码是直接用汇编语言编写的)。 ,我认为您很难找到一个系统,在该系统中方法中是否存在局部变量会产生显着差异。对于重要的变量,该方法将需要具有预期的深度递归的递归。即使这样,在变量本身引起问题之前,也有可能超过堆栈深度,从而导致堆栈溢出异常。唯一可能出现问题的真实情况是使用递归方法在堆栈上分配了非常大的数组。但这也不是不可能的,因为我认为大多数开发人员都会对大型数组的不必要副本三思而后行。

#8 楼

赋值后,s = a + b;变量a和b不再使用。因此,如果您没有使用完全损坏大脑的编译器,则不会将内存用于;无论如何,用于a和b的内存都将被重用。

但是优化此功能完全是胡说八道。如果可以节省空间,则函数运行时可能会占用8个字节(函数返回时会恢复),因此绝对没有意义。如果可以节省时间,那将是一个十亿分之一秒。优化这是在浪费时间。

#9 楼

局部值类型变量是在堆栈上分配的,或者(对于这种小的代码段更可能)使用处理器中的寄存器,而从没有看到任何RAM。无论哪种方式,它们都是短暂的,无需担心。当您需要在可能很大且寿命很长的集合中缓冲或排队数据元素时,就开始考虑使用内存。处理速度?响应时间?内存占用量?可维护性?设计的一致性?一切由您决定。

评论


细说:.NET至少(该帖子的语言未指定)对“在堆栈上”分配局部变量没有任何保证。请参阅Eric Lippert的“栈是实现细节”。

– jrh
18/09/4在22:19



@jrh堆栈或堆上的局部变量可能是实现细节,但是如果有人真的想要堆栈上的变量,则可以使用stackalloc和Span 。分析后在热点中可能有用。另外,有关结构的一些文档暗示值类型可能在堆栈上,而引用类型却不在。无论如何,充其量您最好避免使用GC。

–鲍勃
18-09-6在2:27



#10 楼

正如其他答案所说,您需要考虑要优化的内容。

在此示例中,我怀疑任何体面的编译器都会为这两种方法生成等效的代码,因此该决定将无效在运行时或内存上!

它影响的是代码的可读性。 (代码供人类阅读,而不仅仅是计算机。)两个示例之间没有太大差异;当其他所有条件都相同时,我认为简洁是一种美德,因此我可能会选择方法B. br />
要考虑的事情:


中间表达式是否有副作用?如果它调用任何不纯函数或更新任何变量,那么当然将其复制是正确性的问题,而不仅仅是样式。
中间表达式有多复杂?如果它执行大量计算和/或调用函数,则编译器可能无法对其进行优化,因此这会影响性能。 (不过,正如Knuth所说,“我们应该忘记效率低下,大约有97%的时间。”)
中间变量是否有意义?可以给它一个名称来帮助解释发生了什么吗?简短而翔实的名称可以更好地解释代码,而毫无意义的名称只是视觉干扰。
中间表达式要持续多长时间?如果太长,则复制它会使代码更长且更难阅读(尤其是如果它强制换行);如果不是这样,则整个复制可能会更短。


#11 楼

正如许多答案所指出的那样,尝试使用现代编译器调整此功能不会有任何不同。优化器最有可能找出最佳解决方案(投票给显示汇编代码的答案以证明这一点!)。您说采访中的代码并不完全是您要比较的代码,因此实际示例也许更有意义。

但是让我们再看一下这个问题:这是一个面试问题。所以真正的问题是,假设您想尝试并获得工作,该如何回答?您会知道的。

我会提到,忽略优化器,第一个可能会在堆栈上创建一个临时变量,而第二个则不会,但是会执行两次计算。因此,第一个使用更多的内存,但速度更快。

您可能会提到,无论如何,计算可能需要一个临时变量来存储结果(以便进行比较),因此是否命名

然后我会提到,实际上所有代码都是经过优化的,并且由于所有变量都是局部变量,因此很可能生成等效的机器代码。但是,这确实取决于您使用的编译器(不久前,我可以通过在Java中将局部变量声明为“ final”来获得有用的性能改进)。

您可能会提到该堆栈无论如何都位于其自己的内存页面中,因此,除非您的额外变量导致该堆栈使该页面溢出,否则实际上不会分配更多内存。如果它确实溢出,则将需要一个全新的页面。会引起CPU与内存的问题。

所有这些都说明您知道您在说什么。尽管在这种情况下是正确的,但在采访中它可能会被解释为“我不了解性能,但是我的代码读起来像是珍妮特和约翰的故事”。列出关于如何不需要代码优化的平淡无奇的声明,不要在分析完代码后进行优化(这只是表明您看不到自己的错误代码),硬件成本比程序员少,请不要引用Knuth的“过早等等...”。特别是对于像Amazon这样的组织,某些代码具有巨大的影响力。一个代码片段可以部署在数千个服务器或数百万个设备上,一年中每天可能被调用数十亿次。可能有成千上万的类似片段。好的算法和好的算法之间的差异很容易达到千分之一。将数字和所有数字相乘:这会有所作为。如果系统容量不足,则不良代码组织的潜在成本可能非常高,甚至致命。

此外,许多此类组织在竞争环境中工作。因此,如果竞争对手的软件已经可以在他们拥有的硬件上正常运行,或者该软件可以在手机上运行并且无法升级,您就不能仅仅告诉客户购买一台更大的计算机。某些应用程序特别注重性能(想到游戏和移动应用程序),并且可能会根据其响应性或速度而存活或死亡。

我个人过去二十多年一直在许多项目中工作,这些项​​目由于性能问题而导致系统出现故障或无法使用,并且我曾被要求优化这些系统,在所有情况下,这都是由于程序员的错误代码所致,他们不懂他们所写内容的影响。再者,它从来都不是一段代码,它无处不在。当我出现时,是时候开始考虑性能了:损坏已经造成了。

理解代码性能是一种与理解代码正确性和代码风格相同的好技能。 。它来自实践。性能故障可能与功能故障一样严重。如果系统无法正常工作,那么它将无法正常工作。没关系,为什么。同样,从未使用过的性能和功能都不好。

因此,如果面试官问您有关性能的问题,我建议您尝试并尽可能多地展示知识。如果这个问题看起来很糟糕,请礼貌地指出为什么您认为在这种情况下这不是问题。不要引用Knuth。

#12 楼

您首先应该优化正确性。

对于接近Int.MaxValue的输入值,您的函数失败:

该函数还不适用于a = int.MinValue +200。(错误地加起来为“ 400”) ,但“溢出是真实的”。

在面试时,提出问题以阐明问题的范围:允许的最大和最小输入值是多少?获得这些值后,如果调用方提交的值超出范围,则可以引发异常。或者(在C#中),您可以使用选中的{}部分,这将在溢出时引发异常。是的,这需要更多的工作和复杂的操作,但有时就是这样。

评论


这些方法仅是示例。他们写的不是正确的,而是为了说明实际问题。感谢您的输入!

– Corey P
'18 Sep 6'在17:14

我认为面试问题是针对表现的,因此您需要回答问题的意图。面试官没有询问极限行为。但是无论如何有趣的方面。

–rghome
18-09-7在12:15



@Corey好面试官可以回答以下问题:1)评估有关该问题的候选人能力,正如rghome在此建议的那样; 2)作为较大问题(例如潜意识的功能正确性)和相关知识深度的开口-如此在以后的职业面试中-祝你好运。

–chux-恢复莫妮卡
18/09/7'16:58

#13 楼

您的问题应该是:“我是否需要对此进行优化?”。

版本A和B在一个重要的细节上有所不同,使A更为可取,但与优化无关:您不需要重复代码。

实际的“优化”称为通用子表达式消除,几乎每个编译器都这样做。有些人甚至在优化关闭时也会执行此基本优化。因此,这并不是真正的优化(生成的代码在每种情况下几乎肯定是完全相同的)。

但是,如果不是优化,那为什么更可取呢?好吧,您不在乎任何代码,谁在乎!

首先,您不会冒意外将条件子句的一半弄错的风险。但更重要的是,阅读此代码的人可以立即查看您要尝试执行的操作,而不是if((((wtf||is||this||longexpression))))的体验。读者可以看到的是if(one || theother),这是一件好事。碰巧的是,我是另一个人,三年后又读了自己的代码,然后想“ WTF是什么意思?”。在这种情况下,如果您的代码立即传达意图,那总是有帮助的。可以正确命名一个通用子表达式。
此外,如果将来有任何时候,您可以决定您需要将a+b更改为a-b,您必须更改一个位置,而不是两个。而且也没有(再次)偶然地弄错第二个错误的风险。这是绝对最重要的事情。不正确的代码是错误的代码,甚至更糟糕的是,即使尽管不正确,它也可以正常工作,或者至少看起来像是可以正常工作。之后,代码应该是可读的(不熟悉它的人可以读取)。
至于优化...当然不应该故意编写反优化的代码,当然我并不是说您不应该在开始设计之前就花点时间思考一下(例如为问题选择正确的算法,不是最不高效的代码)。

但是对于大多数应用程序而言,大多数情况下,通过优化的编译器使用合理的算法运行正确,可读的代码后,您获得的性能就很好,没有真正需要担心。

如果不是这种情况,即,如果应用程序的性能确实不符合要求,那么只有这样,您才应该担心像您那样进行局部优化。尝试。不过,最好还是重新考虑顶层算法。如果由于更好的算法而调用一个函数500次而不是50,000次,则与在微优化上节省三个时钟周期相比,其影响更大。如果您不一直在随机存储器访问上停滞数百个周期,那么这会比进行一些廉价的计算等产生更大的影响。可以写有关这本书的整本书,并且永无止境),花时间盲目地优化某个特定位置(甚至根本不知道这是否是瓶颈!)通常是浪费时间。没有概要分析,优化就很难正确实现。

但是,根据经验,当您盲目地只需要/想要做某事时,或者作为一般默认策略,建议针对“内存”进行优化。
优化“内存”(尤其是空间位置和访问模式)通常会带来好处,因为与以前的情况“千篇一律”不同,如今访问RAM是最昂贵的事情之一(缺少从磁盘读取的内容!)您原则上可以做到的。另一方面,ALU价格便宜,而且每周都在增长。内存带宽和延迟的提高速度几乎没有提高。与大量数据应用程序中的不良访问方式相比,良好的位置和良好的访问方式可以在运行时轻松地产生5倍的差异(在极端情况下,人为的示例要高出20倍)。善待您的缓存,您将成为一个快乐的人。

要使上一段正确看待,请考虑一下您可以做的各种不同的事情花费了您什么。执行类似a+b之类的内容需要花费一个或两个周期(如果没有进行优化),但是CPU通常每个周期可以启动几条指令,并且可以流水线化非依赖的指令,因此更现实的是,它仅花费您半个周期或更短的时间。理想情况下,如果编译器擅长调度,并且视情况而定,它可能会为零。如果您不太幸运(L2命中),则大约15个周期。如果数据根本不在缓存中,则将花费数百个周期。如果您的偶然访问模式超出了TLB的能力(只需输入约50个条目即可轻松完成),请再增加几百个周期。如果您的偶然访问模式实际上导致了页面错误,则在最佳情况下将花费一万个周期,在最坏情况下将花费数百万个周期。
现在考虑一下,您最想避免的事情是什么?

#14 楼


什么时候优化内存与方法的性能速度?


首先正确使用功能之后。然后选择性就涉及到微优化。


作为优化的访谈问题,代码确实引起了通常的讨论,但却错过了更高级别的目标,即代码功能是否正确?

C ++和C等人都将int溢出视为来自a + b的问题。它的定义不明确,C称之为未定义的行为。未指定要“换行”-即使这是常见的行为。 IsSumInRange()的所有int值。原始a,b不是。 AC解决方案可以使用:

bool IsSumInRange(int a, int b) {
    int s = a + b;  // Overflow possible
    if (s > 1000 || s < -1000) return false;
    else return true;
}


上面的代码可以通过使用比a + b更宽的整数类型(如果可用)进行优化,如下所示,或者在内部分配intsum > N测试sum < -N逻辑。但是,如果使用智能编译器,这种优化可能并不能真正导致“更快”地发出代码,也不值得为保持聪明而额外维护。 if (a >= 0)时出现问题。

#15 楼

我们在谈论什么样的编译器,以及什么样的“内存”?因为在您的示例中,假设有一个合理的优化程序,所以在进行这种算术运算之前,通常需要将表达式a+b存储在寄存器(一种内存形式)中。

因此,如果我们谈论的是愚蠢的遇到a+b两次的编译器,将在第二个示例中分配更多的寄存器(内存),因为第一个示例可能只将该表达式存储在映射到局部变量的单个寄存器中一次,但是我们在谈论的是非常愚蠢的这一点...除非您正在使用另一种类型的傻瓜编译器,该傻瓜式编译器会在每个位置上溢出所有单个变量,在这种情况下,第一个变量可能比第二个变量更难优化。



我仍然想解决这个问题,并认为第二个可能会在哑堆编译器上使用更多的内存,即使它容易发生堆栈溢出,因为它最终可能会为三个分配寄存器a+b和泄漏ab mo回覆。如果我们使用的是最原始的优化器,则将a+b捕获到s可能会“帮助”使用更少的寄存器/堆栈溢出。
缺少测量/反汇编的方式,甚至在最坏的情况下,这也不是“内存与性能”的情况(因为即使在我能想到的最差的优化器中,我们也没有谈论任何东西,除了堆栈/寄存器),充其量只是纯粹的“性能”案例,在任何合理的优化器中,两者都是等效的,如果不使用合理的优化器,为什么对优化如此迷恋,尤其是缺乏测量,却如此?这就像指令选择/寄存器分配程序集级别的关注点,我从不希望任何人希望在使用例如堆栈溢出的解释器时保持高效。



什么时候可以优化内存与方法的性能?



关于这个问题,我是否可以解决更多从广义上讲,我经常找不到两个截然相反的地方。尤其是如果您的访问模式是顺序的,并且考虑到CPU缓存的速度,通常减少对非平凡输入的顺序处理的字节数,可以(最多)转化为更快地浏览该数据。当然存在断点,如果数据量大得多,交换量小得多,指令量大,则以较大的形式顺序处理以交换较少的指令可能更快。

但是我已经发现许多开发人员往往低估了在这种情况下减少的内存使用量可以转化为成比例的处理时间减少。将性能成本转换为指令而不是将内存访问转换为达到大型LUT的目的是徒劳的,这是徒劳地试图加快一些小型计算的速度,但却发现由于附加的内存访问而导致性能下降。

对于通过某个巨大数组进行顺序访问的情况(不像您的示例那样讲局部标量变量),我遵循这样的规则:顺序进行耕作的内存越少,转换的性能就越高,尤其是当生成的代码比其他方法更简单时,直到我的测量和探查器告诉我否则,它才有意义,并且以同样的方式,我假设顺序读取磁盘上的一个较小的二进制文件会比较大的文件快得多(即使较小的文件需要一些文件)。更多说明),直到该假设在我的测量中不再适用。