แทนที่จะคาดเดาเกี่ยวกับสิ่งที่อาจจะเกิดขึ้นหรือไม่เกิดขึ้นเรามาดูกันดีกว่า ฉันจะต้องใช้ C ++ เพราะฉันไม่มีคอมไพเลอร์ C # สะดวก (แต่ดูตัวอย่าง C #จากVisualMelon ) แต่ฉันแน่ใจว่าใช้หลักการเดียวกันนี้โดยไม่คำนึงถึง
เราจะรวมสองทางเลือกที่คุณพบในการสัมภาษณ์ นอกจากนี้เราจะรวมเวอร์ชันที่ใช้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
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 $0x3e8,-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 $0xfffffc18,-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 $0x0,%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 $0x1,%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 $0x3e8,%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 $0xfffffc18,%eax # compare to -1000
5b: 7d 07 jge 64 <_Z22IsSumInRangeWithoutVarii+0x2f>
5d: b8 00 00 00 00 mov $0x0,%eax
62: eb 05 jmp 69 <_Z22IsSumInRangeWithoutVarii+0x34>
64: b8 01 00 00 00 mov $0x1,%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 $0xfffffc18,%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 $0x3e8,%eax
91: 7f 07 jg 9a <_Z26IsSumInRangeSuperOptimizedii+0x2f>
93: b8 01 00 00 00 mov $0x1,%eax
98: eb 05 jmp 9f <_Z26IsSumInRangeSuperOptimizedii+0x34>
9a: b8 00 00 00 00 mov $0x0,%eax
9f: 5d pop %rbp
a0: c3 retq
เราจะเห็นได้จากที่อยู่สแต็ค (ตัวอย่างเช่น-0x4
ในmov %edi,-0x4(%rbp)
เมื่อเทียบกับ-0x14
ในmov %edi,-0x14(%rbp)
) ที่IsSumInRangeWithVar()
ใช้ 16 ไบต์พิเศษในกอง
เนื่องจากIsSumInRangeWithoutVar()
ไม่มีการจัดสรรพื้นที่บนสแต็กเพื่อจัดเก็บค่ากลางs
จึงจำเป็นต้องคำนวณใหม่ส่งผลให้การใช้งานนี้มี 2 คำแนะนำอีกต่อไป
ตลกIsSumInRangeSuperOptimized()
ดูคล้าย ๆ มากIsSumInRangeWithoutVar()
ยกเว้นจะเปรียบเทียบกับ -1000 ก่อนและ 1,000 วินาที
ตอนนี้เรามารวมกันกับการเพิ่มประสิทธิภาพขั้นพื้นฐานที่สุดเท่านั้น: g++ -O1 -c -o test.o test.cpp
. ผลลัพธ์:
0000000000000000 <_Z19IsSumInRangeWithVarii>:
0: 8d 84 37 e8 03 00 00 lea 0x3e8(%rdi,%rsi,1),%eax
7: 3d d0 07 00 00 cmp $0x7d0,%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 $0x7d0,%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 $0x7d0,%eax
2c: 0f 96 c0 setbe %al
2f: c3 retq
คุณจะดูว่าแต่ละคนที่แตกต่างเป็นเหมือนกัน คอมไพเลอร์สามารถทำสิ่งที่ค่อนข้างฉลาด: abs(a + b) <= 1000
เทียบเท่ากับการa + b + 1000 <= 2000
พิจารณาsetbe
ทำการเปรียบเทียบที่ไม่ได้ลงนามดังนั้นจำนวนลบกลายเป็นจำนวนบวกที่มาก การlea
เรียนการสอนสามารถดำเนินการเพิ่มเติมเหล่านี้ทั้งหมดในหนึ่งคำสั่งและกำจัดสาขาตามเงื่อนไขทั้งหมด
เพื่อที่จะตอบคำถามของคุณมักจะเป็นสิ่งที่จะเพิ่มประสิทธิภาพสำหรับหน่วยความจำไม่หรือความเร็ว แต่อ่านง่าย รหัสการอ่านนั้นยากกว่าการเขียนมากและการอ่านรหัสที่มีการจัดการเพื่อ "เพิ่มประสิทธิภาพ" นั้นจะยากกว่าการอ่านรหัสที่เขียนเพื่อให้ชัดเจน บ่อยครั้งกว่า "การเพิ่มประสิทธิภาพ" เหล่านี้มีความสำคัญน้อยมากหรือในกรณีนี้ไม่มีผลกระทบต่อประสิทธิภาพที่แท้จริง
คำถามติดตามสิ่งที่เปลี่ยนแปลงเมื่อรหัสนี้เป็นภาษาตีความแทนการรวบรวม? จากนั้นการเพิ่มประสิทธิภาพมีความสำคัญหรือมีผลเหมือนกันหรือไม่
มาวัดกัน! ฉันได้คัดลอกตัวอย่างไปที่ Python:
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)),))
รันด้วย Python 3.5.2 สิ่งนี้จะสร้างผลลัพธ์:
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
การถอดชิ้นส่วนใน Python นั้นไม่น่าสนใจอย่างมากเนื่องจากชุดประมวลผลแบบ "รหัส" ไม่ได้มีประโยชน์ในการเพิ่มประสิทธิภาพมากนัก
ประสิทธิภาพของทั้งสามฟังก์ชั่นนั้นเกือบจะเหมือนกัน เราอาจถูกล่อลวงให้ไปIsSumInRangeWithVar()
เพราะมันมีความเร็วเพิ่มขึ้นเล็กน้อย แม้ว่าฉันจะเพิ่มเมื่อฉันพยายามพารามิเตอร์ที่แตกต่างกันtimeit
แต่บางครั้งIsSumInRangeSuperOptimized()
ก็ออกมาเร็วที่สุดดังนั้นฉันสงสัยว่ามันอาจเป็นปัจจัยภายนอกที่รับผิดชอบต่อความแตกต่างมากกว่าข้อได้เปรียบที่แท้จริงของการใช้งานใด ๆ
หากนี่เป็นโค้ดที่มีประสิทธิภาพที่สำคัญจริงๆภาษาที่ตีความแล้วเป็นตัวเลือกที่แย่มาก ใช้โปรแกรมเดียวกันกับ pypy ฉันได้รับ:
IsSumInRangeWithVar: 0.000180s
IsSumInRangeWithoutVar: 0.001175s
IsSumInRangeSuperOptimized: 0.001306s
เพียงแค่ใช้ pypy ซึ่งใช้การรวบรวม JIT เพื่อกำจัดค่าใช้จ่ายของล่ามจำนวนมากได้ให้การปรับปรุงประสิทธิภาพของขนาด 1 หรือ 2 คำสั่ง ฉันค่อนข้างตกใจที่เห็นIsSumInRangeWithVar()
ลำดับความสำคัญเร็วกว่าคนอื่น ดังนั้นฉันจึงเปลี่ยนลำดับของการวัดประสิทธิภาพและวิ่งอีกครั้ง:
IsSumInRangeSuperOptimized: 0.000191s
IsSumInRangeWithoutVar: 0.001174s
IsSumInRangeWithVar: 0.001265s
ดังนั้นดูเหมือนว่ามันไม่ได้มีอะไรเกี่ยวกับการใช้งานจริงที่ทำให้มันเร็ว แต่เป็นลำดับที่ฉันทำการเปรียบเทียบ!
ฉันชอบที่จะขุดลึกลงไปในเรื่องนี้มากขึ้นเพราะฉันไม่รู้ว่าทำไมสิ่งนี้ถึงเกิดขึ้น แต่ฉันเชื่อว่ามีการสร้างประเด็นขึ้นมา: การปรับให้เหมาะสมแบบไมโครเช่นการประกาศค่ากลางว่าเป็นตัวแปรหรือไม่เกี่ยวข้องกันน้อยมาก ด้วยภาษาที่แปลหรือคอมไพเลอร์ที่ได้รับการปรับปรุงอย่างดีวัตถุประสงค์แรกยังคงเขียนโค้ดที่ชัดเจน
หากการเพิ่มประสิทธิภาพต่อไปอาจจะต้องมาตรฐาน โปรดจำไว้ว่าการเพิ่มประสิทธิภาพที่ดีที่สุดนั้นไม่ได้มาจากรายละเอียดเล็ก ๆ น้อย ๆ แต่ภาพอัลกอริทึมที่ใหญ่กว่า: pypy จะเป็นลำดับของขนาดที่เร็วกว่าสำหรับการประเมินฟังก์ชั่นซ้ำ ๆ ซ้ำ ๆ กันกว่า cpython เพราะมันใช้อัลกอริทึมที่เร็วขึ้น โครงการ และมีอัลกอริธึมการเข้ารหัสที่ต้องพิจารณาด้วยเช่นกันการค้นหาผ่านต้นไม้ B จะเร็วกว่ารายการที่เชื่อมโยง
หลังจากทำให้แน่ใจว่าคุณกำลังใช้เครื่องมือและอัลกอริธึมที่เหมาะสมสำหรับงานเตรียมพร้อมที่จะดำน้ำลึกลงไปในรายละเอียดของระบบ ผลลัพธ์อาจน่าประหลาดใจมากแม้แต่สำหรับนักพัฒนาที่มีประสบการณ์และนี่คือเหตุผลที่คุณต้องมีเกณฑ์มาตรฐานเพื่อประเมินการเปลี่ยนแปลง