เมื่อใดจึงควรปรับให้เหมาะสมสำหรับหน่วยความจำและประสิทธิภาพความเร็วสำหรับวิธีการ?


107

ฉันเพิ่งสัมภาษณ์ที่อเมซอน ในระหว่างเซสชันการเข้ารหัสผู้สัมภาษณ์ถามว่าทำไมฉันถึงประกาศตัวแปรในวิธีหนึ่ง ฉันอธิบายกระบวนการของฉันและเขาท้าทายให้ฉันแก้ปัญหาเดียวกันโดยมีตัวแปรน้อยลง ยกตัวอย่างเช่น (นี่คือไม่ได้มาจากการสัมภาษณ์) ผมเริ่มต้นด้วยวิธี A:แล้วดีขึ้นก็จะวิธีการ Bint sโดยการเอา เขายินดีและกล่าวว่าวิธีนี้จะลดการใช้หน่วยความจำด้วยวิธีนี้

ฉันเข้าใจตรรกะเบื้องหลัง แต่คำถามของฉันคือ:

เมื่อใดจึงเหมาะสมที่จะใช้วิธี A กับวิธี B และในทางกลับกัน

คุณจะเห็นว่าA วิธีการที่เป็นไปได้ใช้หน่วยความจำที่สูงขึ้นเนื่องจากint sมีการประกาศ 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;
}

229
ฉันยินดีที่จะเดิมพันว่าคอมไพเลอร์สมัยใหม่จะสร้างชุดประกอบเดียวกันสำหรับทั้งสองกรณี
17 ของ 26

12
ฉันย้อนคำถามกลับสู่สถานะเดิมเนื่องจากการแก้ไขของคุณทำให้คำตอบของฉันไม่ถูกต้อง - โปรดอย่าทำอย่างนั้น! หากคุณถามคำถามเกี่ยวกับวิธีปรับปรุงรหัสของคุณอย่าเปลี่ยนคำถามโดยปรับปรุงรหัสตามวิธีที่แสดงซึ่งทำให้คำตอบไม่มีความหมาย
Doc Brown

76
รอสักครู่พวกเขาขอให้กำจัดint sในขณะที่กำลังดีกับตัวเลขเวทย์มนตร์สำหรับขอบเขตบนและล่าง?
null

34
เตือนความจำ: โปรไฟล์ก่อนที่จะปรับให้เหมาะสม ด้วยคอมไพเลอร์ที่ทันสมัยวิธี A และวิธี B อาจได้รับการปรับให้เหมาะกับรหัสเดียวกัน (โดยใช้ระดับการเพิ่มประสิทธิภาพที่สูงขึ้น) นอกจากนี้ด้วยโปรเซสเซอร์ที่ทันสมัยพวกเขาสามารถมีคำสั่งที่ทำงานได้ดีกว่าการใช้งานเพียงครั้งเดียว
Thomas Matthews

142
ทั้ง; ปรับให้เหมาะสมสำหรับการอ่าน
แอนดี้

คำตอบ:


148

แทนที่จะคาดเดาเกี่ยวกับสิ่งที่อาจจะเกิดขึ้นหรือไม่เกิดขึ้นเรามาดูกันดีกว่า ฉันจะต้องใช้ 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 จะเร็วกว่ารายการที่เชื่อมโยง

หลังจากทำให้แน่ใจว่าคุณกำลังใช้เครื่องมือและอัลกอริธึมที่เหมาะสมสำหรับงานเตรียมพร้อมที่จะดำน้ำลึกลงไปในรายละเอียดของระบบ ผลลัพธ์อาจน่าประหลาดใจมากแม้แต่สำหรับนักพัฒนาที่มีประสบการณ์และนี่คือเหตุผลที่คุณต้องมีเกณฑ์มาตรฐานเพื่อประเมินการเปลี่ยนแปลง


6
เมื่อต้องการให้ตัวอย่างใน C #: SharpLab สร้าง asm ที่เหมือนกันสำหรับทั้งสองวิธี (เดสก์ท็อป CLR v4.7.3130.00 (clr.dll) บน x86)
VisualMelon

2
@VisualMelon สนุกพอที่การตรวจสอบในเชิงบวก: "return (((a + b)> = -1000) && ((a + b) <= 1,000));" ให้ผลลัพธ์ที่แตกต่าง : sharplab.io/…
Pieter B

12
ความสามารถในการอ่านอาจทำให้โปรแกรมเพิ่มประสิทธิภาพได้ง่ายขึ้นเช่นกัน คอมไพเลอร์สามารถเขียนการใช้งานเทียบเท่าตรรกะเช่นนั้นอยู่เหนือ แต่ถ้ามันเป็นจริงสามารถคิดออกสิ่งที่คุณกำลังพยายามที่จะทำ หากคุณใช้bithacks โรงเรียนเก่าจำนวนมากโยนกลับไปกลับมาระหว่าง ints และพอยน์เตอร์การนำหน่วยเก็บข้อมูลที่ไม่แน่นอนกลับมาใช้ใหม่ ฯลฯ อาจเป็นเรื่องยากสำหรับคอมไพเลอร์ที่จะพิสูจน์ว่าการแปลงนั้นเทียบเท่ากันและมันจะทิ้งสิ่งที่คุณเขียน ซึ่งอาจไม่ดี
Leushenko

1
@Corey ดูการแก้ไข
Phil

2
@Corey: คำตอบนี้จริง ๆ แล้วบอกคุณว่าสิ่งที่ฉันเขียนไว้ในคำตอบของฉัน: ไม่มีความแตกต่างเมื่อคุณใช้คอมไพเลอร์ที่ดีและแทนที่จะมุ่งเน้นไปที่ความพร้อม แน่นอนมันดูดีขึ้นแล้ว - บางทีคุณอาจจะเชื่อฉัน
Doc Brown

67

ในการตอบคำถามที่ระบุไว้:

เมื่อใดจึงควรปรับให้เหมาะสมสำหรับหน่วยความจำและประสิทธิภาพความเร็วสำหรับวิธีการ?

มีสองสิ่งที่คุณต้องสร้าง:

  • ใบสมัครของคุณมีข้อ จำกัด อะไร?
  • ฉันจะเรียกคืนทรัพยากรส่วนใหญ่ได้ที่ไหน

ในการตอบคำถามแรกคุณต้องรู้ว่าข้อกำหนดด้านประสิทธิภาพสำหรับใบสมัครของคุณคืออะไร หากไม่มีข้อกำหนดด้านประสิทธิภาพก็ไม่มีเหตุผลที่จะต้องปรับให้เหมาะสมไม่ทางใดก็ทางหนึ่ง ข้อกำหนดด้านประสิทธิภาพช่วยให้คุณไปถึงสถานที่ "ดีพอ"

วิธีการที่คุณระบุไว้จะไม่ทำให้เกิดปัญหาประสิทธิภาพ แต่อย่างใด แต่ภายในวงวนและประมวลผลข้อมูลจำนวนมากคุณต้องเริ่มคิดแตกต่างกันเล็กน้อยเกี่ยวกับวิธีที่คุณกำลังเข้าใกล้ปัญหา

การตรวจสอบสิ่งที่ จำกัด แอปพลิเคชัน

เริ่มดูพฤติกรรมของแอปพลิเคชันของคุณด้วยการตรวจสอบประสิทธิภาพ คอยดูการใช้งาน CPU, ดิสก์, เครือข่ายและหน่วยความจำขณะที่ทำงาน ไอเท็มหนึ่งรายการขึ้นไปจะถูกใช้งานได้สูงสุดในขณะที่ทุกอย่างถูกใช้อย่างเหมาะสมยกเว้นว่าคุณได้รับความสมดุลอย่างสมบูรณ์แบบ แต่แทบจะไม่เกิดขึ้นเลย

เมื่อคุณจำเป็นต้องมองลึกโดยทั่วไปแล้วคุณจะใช้Profiler มีตัวรวบรวมหน่วยความจำและตัวสร้างโปรไฟล์กระบวนการและพวกเขาวัดสิ่งต่าง ๆ การทำโปรไฟล์มีผลกระทบต่อประสิทธิภาพอย่างมาก แต่คุณกำลังใช้รหัสเพื่อค้นหาว่ามีอะไรผิดปกติ

สมมติว่าคุณเห็นการใช้งาน CPU และดิสก์ของคุณสูงสุด คุณต้องตรวจสอบ "ฮอตสปอต" หรือรหัสที่เรียกบ่อยกว่าที่เหลือหรือใช้เวลาในการประมวลผลนานขึ้นอย่างมีนัยสำคัญ

หากคุณไม่พบฮ็อตสปอตใด ๆ จากนั้นคุณจะเริ่มดูที่ความทรงจำ บางทีคุณกำลังสร้างวัตถุมากกว่าที่จำเป็นและการเก็บขยะของคุณทำงานล่วงเวลา

การเรียกคืนประสิทธิภาพ

คิดวิเคราะห์ รายการการเปลี่ยนแปลงต่อไปนี้คือลำดับผลตอบแทนจากการลงทุนที่คุณจะได้รับ:

  • สถาปัตยกรรม: มองหาจุดที่ทำให้หายใจไม่ออก
  • อัลกอริทึม: วิธีที่คุณประมวลผลข้อมูลอาจจำเป็นต้องเปลี่ยนแปลง
  • ฮอตสปอต: การลดความถี่ที่คุณเรียกว่าฮอตสปอตให้น้อยที่สุดสามารถให้โบนัสใหญ่ได้
  • การปรับให้เหมาะสมแบบ Micro: มันไม่ธรรมดา แต่บางครั้งคุณต้องคิดถึงการปรับแต่งเล็กน้อย (เช่นตัวอย่างที่คุณให้ไว้) โดยเฉพาะอย่างยิ่งถ้ามันเป็นจุดร้อนในรหัสของคุณ

ในสถานการณ์เช่นนี้คุณต้องใช้วิธีการทางวิทยาศาสตร์ มาพร้อมกับสมมติฐานทำการเปลี่ยนแปลงและทดสอบ หากคุณบรรลุเป้าหมายด้านประสิทธิภาพแสดงว่าคุณทำเสร็จแล้ว ถ้าไม่ไปที่สิ่งต่อไปในรายการ


ตอบคำถามอย่างกล้าหาญ:

เมื่อใดจึงเหมาะสมที่จะใช้วิธี A กับวิธี B และในทางกลับกัน

จริงๆแล้วนี่เป็นขั้นตอนสุดท้ายในการพยายามจัดการกับปัญหาด้านประสิทธิภาพหรือความจำ ผลกระทบของวิธี A กับวิธี B จะแตกต่างกันขึ้นอยู่กับภาษาและแพลตฟอร์ม (ในบางกรณี)

เพียงใดเกี่ยวกับภาษาที่รวบรวมกับเพิ่มประสิทธิภาพดีครึ่งหนึ่งจะสร้างรหัสที่คล้ายกันกับทั้งโครงสร้างเหล่านั้น อย่างไรก็ตามสมมติฐานเหล่านั้นไม่จำเป็นต้องเป็นจริงในภาษาที่เป็นกรรมสิทธิ์และของเล่นที่ไม่มีเครื่องมือเพิ่มประสิทธิภาพ

แม่นยำซึ่งจะมีผลกระทบที่ดีขึ้นอยู่กับว่าsumตัวแปร stack หรือตัวแปร heap นี่คือตัวเลือกการใช้ภาษา ใน C, C ++ และ Java ตัวอย่างเช่นจำนวนดั้งเดิมเช่นintตัวแปรสแต็กโดยค่าเริ่มต้น รหัสของคุณไม่มีผลกระทบต่อหน่วยความจำอีกต่อไปโดยการกำหนดให้กับตัวแปรสแต็กมากกว่าที่คุณจะได้รับด้วยรหัสแบบอินไลน์อย่างสมบูรณ์

การเพิ่มประสิทธิภาพอื่น ๆ ที่คุณอาจพบในไลบรารี C (โดยเฉพาะที่เก่ากว่า) ซึ่งคุณสามารถตัดสินใจได้ระหว่างการคัดลอกอาเรย์ 2 มิติก่อนหรือข้ามก่อนคือการปรับให้เหมาะสมขึ้นอยู่กับแพลตฟอร์ม มันต้องมีความรู้ว่าชิปเซ็ตที่คุณตั้งเป้าหมายจะใช้งานหน่วยความจำได้ดีที่สุดอย่างไร มีความแตกต่างเล็กน้อยระหว่างสถาปัตยกรรม

บรรทัดล่างคือการเพิ่มประสิทธิภาพคือการรวมกันของศิลปะและวิทยาศาสตร์ มันต้องใช้การคิดเชิงวิพากษ์รวมถึงระดับความยืดหยุ่นในการเข้าหาปัญหา มองหาเรื่องใหญ่ก่อนที่คุณจะตำหนิเรื่องเล็ก ๆ


2
คำตอบนี้มุ่งเน้นที่คำถามของฉันมากที่สุดและไม่ติดอยู่กับตัวอย่างโค้ดของฉันเช่นวิธี A และวิธี B
Corey P

18
ฉันรู้สึกว่านี่เป็นคำตอบทั่วไปของ "คุณจะจัดการกับปัญหาคอขวดของประสิทธิภาพการทำงานอย่างไร" แต่คุณจะยากที่จะระบุการใช้หน่วยความจำแบบสัมพัทธ์จากฟังก์ชั่นเฉพาะโดยพิจารณาว่ามีตัวแปร 4 หรือ 5 ตัวที่ใช้วิธีนี้ ฉันยังถามด้วยว่าการเพิ่มประสิทธิภาพระดับนี้มีความเกี่ยวข้องเพียงใดเมื่อคอมไพเลอร์ (หรือล่าม) อาจหรือไม่ปรับให้เหมาะสม
Eric

@Eric ดังที่ฉันได้กล่าวไว้หมวดหมู่สุดท้ายของการปรับปรุงประสิทธิภาพคือการเพิ่มประสิทธิภาพขนาดเล็กของคุณ วิธีเดียวที่จะคาดเดาได้ดีว่าจะมีผลกระทบหรือไม่คือการวัดประสิทธิภาพ / หน่วยความจำใน profiler เป็นเรื่องยากที่การปรับปรุงเหล่านั้นมีผลตอบแทน แต่ในปัญหาประสิทธิภาพที่มีความละเอียดอ่อนตามเวลาที่คุณมีในเครื่องจำลองการเปลี่ยนแปลงที่วางไว้อย่างดีเช่นนั้นอาจเป็นความแตกต่างระหว่างการกดปุ่มเป้าหมายของคุณไม่ใช่ ฉันคิดว่าฉันสามารถนับจำนวนครั้งในการทำงานกับซอฟต์แวร์ได้มากกว่า 20 ปี แต่ก็ไม่เป็นศูนย์
Berin Loritsch

@BerinLoritsch อีกครั้งโดยทั่วไปฉันเห็นด้วยกับคุณ แต่ในกรณีนี้ฉันไม่ได้ทำ ฉันได้รับคำตอบของฉันเอง แต่ฉันไม่ได้เห็นเครื่องมือใด ๆ ที่จะตั้งค่าสถานะหรือให้วิธีการระบุปัญหาด้านประสิทธิภาพที่เกี่ยวข้องกับขนาดหน่วยความจำสแต็กของฟังก์ชัน
Eric

@DocBrown ฉันได้แก้ไขแล้ว เกี่ยวกับคำถามที่สองฉันเห็นด้วยกับคุณมาก
Berin Loritsch

45

"นี่จะลดหน่วยความจำ" - em, no แม้ว่านี่จะเป็นจริง (ซึ่งสำหรับคอมไพเลอร์ที่เหมาะสมไม่ได้เป็น) ความแตกต่างอาจจะเล็กน้อยสำหรับสถานการณ์โลกแห่งความจริงใด ๆ

อย่างไรก็ตามฉันขอแนะนำให้ใช้วิธี A * (วิธี 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ชื่อที่อธิบายตัวแปรรหัสจะชัดเจนขึ้น

  • มันหลีกเลี่ยงที่จะมีตรรกะการสรุปรวมที่เหมือนกันสองครั้งในโค้ดดังนั้นโค้ดจะกลายเป็น DRY มากกว่าซึ่งหมายถึงข้อผิดพลาดน้อยที่จะเกิดการเปลี่ยนแปลง


36
ฉันจะทำความสะอาดให้ดียิ่งขึ้นและไปที่ "return sum> -1000 && sum <1000;"
17 จาก 26

36
@Corey เครื่องมือเพิ่มประสิทธิภาพที่เหมาะสมใด ๆ จะใช้การลงทะเบียน CPU สำหรับsumตัวแปรจึงนำไปสู่การใช้หน่วยความจำศูนย์ และแม้ว่าจะไม่เป็นเช่นนั้นนี่เป็นเพียงคำศัพท์เดียวในวิธี "leaf" เมื่อพิจารณาว่า Java หรือ C # ที่สิ้นเปลืองหน่วยความจำอย่างไม่น่าเชื่อสามารถเป็นอย่างอื่นได้อย่างไรเนื่องจาก GC และโมเดลวัตถุของพวกเขาintตัวแปรโลคอลไม่ได้ใช้หน่วยความจำที่สังเกตเห็นได้อย่างแท้จริง นี่คือการเพิ่มประสิทธิภาพขนาดเล็กที่ไม่มีจุดหมาย
amon

10
@Corey: ถ้ามันเป็น " ซับซ้อนกว่าเล็กน้อย " ก็อาจจะไม่กลายเป็น "การใช้หน่วยความจำที่เห็นได้ชัด" บางทีถ้าคุณสร้างตัวอย่างที่ซับซ้อนกว่านี้จริงๆ แต่นั่นทำให้มันเป็นคำถามที่แตกต่าง โปรดทราบด้วยเนื่องจากคุณไม่ได้สร้างตัวแปรเฉพาะสำหรับนิพจน์เพื่อให้ได้ผลลัพธ์ระดับกลางที่ซับซ้อนสภาพแวดล้อมเวลาทำงานอาจยังคงสร้างวัตถุชั่วคราวภายในดังนั้นมันจึงขึ้นอยู่กับรายละเอียดของภาษาสิ่งแวดล้อมระดับการเพิ่มประสิทธิภาพและ สิ่งที่คุณเรียกว่า "ชัดเจน"
Doc Brown

8
นอกจากประเด็นข้างต้นฉันค่อนข้างมั่นใจว่า C # / Java เลือกที่จะเก็บไว้sumเป็นรายละเอียดการดำเนินการอย่างไรและฉันสงสัยว่าใคร ๆ ก็สามารถสร้างกรณีที่น่าเชื่อถือได้ว่าเคล็ดลับโง่ ๆ เช่นการหลีกเลี่ยงคนท้องถิ่นintจะนำไปสู่ จำนวนหน่วยความจำที่ใช้ในระยะยาว การอ่าน IMO มีความสำคัญมากกว่า ความสามารถในการอ่านอาจเป็นเรื่องส่วนตัว แต่ FWIW โดยส่วนตัวแล้วฉันอยากให้คุณไม่เคยทำการคำนวณแบบเดียวกันสองครั้งไม่ใช่เพื่อการใช้งาน CPU แต่เพราะฉันต้องตรวจสอบการเพิ่มของคุณอีกครั้งเมื่อฉันกำลังมองหาจุดบกพร่อง
jrh

2
... ยังทราบด้วยว่าภาษาที่เก็บขยะโดยทั่วไปเป็นสิ่งที่คาดเดาไม่ได้ "ปั่นป่วนทะเลแห่งความทรงจำ" ที่ (สำหรับ C # ต่อไป) อาจทำความสะอาดได้เมื่อจำเป็นฉันจำได้ว่าการสร้างโปรแกรมที่จัดสรรแรมกิกะไบต์และเริ่มเท่านั้น " การล้าง "หลังจากตัวเองเมื่อหน่วยความจำเริ่มขาดแคลน หาก GC ไม่จำเป็นต้องเรียกใช้อาจต้องใช้เวลาพอสมควรและประหยัด CPU ของคุณสำหรับเรื่องเร่งด่วนอื่น ๆ
jrh

35

คุณสามารถทำได้ดีกว่าทั้งสองอย่างด้วย

return (abs(a + b) > 1000);

โปรเซสเซอร์ส่วนใหญ่ (และคอมไพเลอร์ด้วยเหตุนี้) สามารถทำ abs () ในการดำเนินการเดียว คุณไม่เพียง แต่มีจำนวนเงินน้อยลง แต่ยังมีการเปรียบเทียบน้อยลงซึ่งโดยทั่วไปมีราคาแพงกว่าการคำนวณ นอกจากนี้ยังลบกิ่งซึ่งเป็นสิ่งที่เลวร้ายมากในโปรเซสเซอร์ส่วนใหญ่เพราะมันจะหยุด pipelining เป็นไปได้

ผู้สัมภาษณ์ดังที่คำตอบอื่น ๆ ได้กล่าวไว้คือชีวิตของพืชและไม่มีธุรกิจที่ดำเนินการสัมภาษณ์ทางเทคนิค

ที่กล่าวว่าคำถามของเขาถูกต้อง และคำตอบเมื่อคุณปรับให้เหมาะสมและวิธีคือเมื่อคุณพิสูจน์แล้วว่ามีความจำเป็นและคุณได้ทำโปรไฟล์เพื่อพิสูจน์ว่าส่วนใดที่จำเป็นต้องใช้ นู ธ กล่าวอย่างมีชื่อเสียงว่าการเพิ่มประสิทธิภาพก่อนวัยอันควรเป็นรากเหง้าของความชั่วร้ายทั้งหมดเพราะมันง่ายเกินไปที่จะลองใช้แผ่นทองคำที่ไม่สำคัญหรือทำการเปลี่ยนแปลง (เช่นผู้สัมภาษณ์ของคุณ) ซึ่งไม่มีผลในขณะที่ไม่มีสถานที่ที่ต้องการจริงๆ จนกว่าคุณจะมีหลักฐานที่พิสูจน์ได้ว่ามีความจำเป็นจริงๆความชัดเจนของรหัสคือเป้าหมายที่สำคัญกว่า

แก้ไข FabioTurati ชี้ให้เห็นอย่างถูกต้องว่านี่เป็นตรรกะที่ตรงกันข้ามกับของจริง (ความผิดพลาดของฉัน!) และสิ่งนี้แสดงให้เห็นถึงผลกระทบเพิ่มเติมจากคำพูดของ Knuth ที่เราเสี่ยงต่อการแตกรหัสในขณะที่เรากำลังพยายามเพิ่มประสิทธิภาพ


2
@Corey ฉันค่อนข้างแน่ใจว่า Graham หมุดคำขอ"เขาท้าทายให้ฉันแก้ปัญหาเดียวกันโดยมีตัวแปรน้อยลง"ตามที่คาดไว้ หากฉันเป็นผู้สัมภาษณ์ฉันคาดหวังคำตอบนั้นไม่ย้ายa+bเข้ามาifและทำมันสองครั้ง คุณเข้าใจว่าผิด"เขายินดีและบอกว่าวิธีนี้จะลดการใช้หน่วยความจำด้วยวิธีนี้" - เขายินดีที่คุณซ่อนความผิดหวังด้วยคำอธิบายที่ไม่มีความหมายนี้เกี่ยวกับหน่วยความจำ คุณไม่ควรจริงจังที่จะถามคำถามที่นี่ คุณได้งานหรือไม่ ฉันเดาว่าคุณไม่ได้ :-(
Sinatr

1
คุณกำลังใช้การแปลง 2 แบบในเวลาเดียวกัน: คุณได้เปลี่ยนเงื่อนไข 2 ข้อเป็น 1 โดยใช้abs()และคุณยังมีเงื่อนไขเดียวreturnแทนที่จะเป็นกรณีที่เงื่อนไขเป็นจริง ("ถ้าสาขา") และอีกกรณีหนึ่งเมื่อเป็นเท็จ ( "สาขาอื่น") เมื่อคุณเปลี่ยนรหัสเช่นนี้โปรดระวัง: มีความเสี่ยงในการเขียนฟังก์ชันที่ส่งคืนจริงโดยไม่ตั้งใจเมื่อมันควรกลับเท็จและในทางกลับกัน ซึ่งเป็นสิ่งที่เกิดขึ้นตรงนี้ ฉันรู้ว่าคุณกำลังมุ่งเน้นไปที่สิ่งอื่นและคุณทำได้ดีมาก ถึงกระนั้นสิ่งนี้อาจทำให้คุณเสียค่าใช้จ่ายได้อย่างง่ายดาย ...
Fabio Turati

2
@ FabioTurati ด่างดีมาก - ขอบคุณ! ฉันจะอัปเดตคำตอบ และเป็นจุดที่ดีเกี่ยวกับการปรับโครงสร้างและการเพิ่มประสิทธิภาพซึ่งทำให้การเสนอราคาของ Knuth มีความเกี่ยวข้องมากยิ่งขึ้น เราควรพิสูจน์ว่าเราต้องการการปรับให้เหมาะสมก่อนที่จะรับความเสี่ยง
เกรแฮม

2
โปรเซสเซอร์ส่วนใหญ่ (และคอมไพเลอร์ด้วยเหตุนี้) สามารถทำ abs () ในการดำเนินการเดียว น่าเสียดายที่ไม่ใช่สำหรับจำนวนเต็ม ARM64 มีเงื่อนไขลบล้างมันสามารถใช้หากมีการตั้งธงไว้แล้วจากaddsและ ARM ได้บอกกล่าวกลับย่อย ( rsblt= ย้อนกลับย่อยถ้าน้อย-tha) แต่ทุกอย่างอื่นต้องมีคำแนะนำพิเศษหลายที่จะดำเนินการหรือabs(a+b) godbolt.org/z/Ok_Conแสดง x86, ARM, AArch64, PowerPC, MIPS และ RISC-V asm เอาต์พุต เป็นเพียงการเปลี่ยนการเปรียบเทียบเป็นช่วงตรวจสอบว่า gcc สามารถปรับให้เหมาะสมเหมือนในคำตอบของ Phil abs(a)(unsigned)(a+b+999) <= 1998U
Peter Cordes

2
รหัส "ที่ปรับปรุงแล้ว" ในคำตอบนี้ยังคงไม่ถูกต้องเนื่องจากเป็นคำตอบที่ต่างออกIsSumInRange(INT_MIN, 0)ไป ต้นฉบับผลตอบแทนรหัสfalseเพราะINT_MIN+0 > 1000 || INT_MIN+0 < -1000; แต่ "ใหม่และการปรับปรุง" รหัสส่งกลับเพราะtrue abs(INT_MIN+0) < 1000(หรือในบางภาษาก็จะมีข้อยกเว้นหรือมีพฤติกรรมที่ไม่ได้กำหนดตรวจสอบรายชื่อท้องถิ่นของคุณ)
Quuxplusone

16

เมื่อใดจึงเหมาะสมที่จะใช้วิธี A กับวิธี B และในทางกลับกัน

ฮาร์ดแวร์ราคาถูก โปรแกรมเมอร์ที่มีราคาแพง ดังนั้นเวลาที่คุณเสียไปกับคำถามนี้อาจจะแย่กว่าคำตอบทั้งคู่

โดยไม่คำนึงถึงคอมไพเลอร์สมัยใหม่ส่วนใหญ่จะหาวิธีเพิ่มประสิทธิภาพตัวแปรโลคัลลงในรีจิสเตอร์ (แทนที่จะจัดสรรพื้นที่สแต็ก) ดังนั้นเมธอดนั้นอาจเหมือนกันในแง่ของโค้ดที่เรียกใช้งานได้ ด้วยเหตุผลนี้นักพัฒนาส่วนใหญ่จะเลือกตัวเลือกที่สื่อสารความตั้งใจอย่างชัดเจนที่สุด (ดูการเขียนโค้ดที่ชัดเจนจริงๆ (ROC) ) ในความคิดของฉันนั่นจะเป็นวิธี A

ในทางตรงกันข้ามถ้านี่เป็นการฝึกเชิงวิชาการอย่างแท้จริงคุณสามารถมีทั้งสองโลกที่ดีที่สุดด้วยวิธี C:

private bool IsSumInRange(int a, int b)
{
    a += b;
    return (a >= -1000 && a <= 1000);
}

17
a+=bเป็นเคล็ดลับที่เรียบร้อย แต่ฉันต้องพูดถึง (ในกรณีที่มันไม่ได้บอกเป็นนัยจากคำตอบที่เหลือ) จากวิธีการใช้งานของฉันที่ยุ่งกับพารามิเตอร์อาจยากที่จะแก้ไขและบำรุงรักษา
jrh

1
ฉันเห็นด้วย @jrh ฉันเป็นผู้สนับสนุนที่แข็งแกร่งให้กับ ROC และสิ่งนั้นก็เป็นอะไร
John Wu

3
"ฮาร์ดแวร์ราคาถูกโปรแกรมเมอร์มีราคาแพง" ในโลกของสินค้าอิเล็กทรอนิกส์สำหรับผู้บริโภคคำแถลงนั้นเป็นเท็จ หากคุณขายล้านหน่วยมันเป็นการลงทุนที่ดีมากที่จะใช้จ่าย $ 500,000 ในการพัฒนาเพิ่มเติมเพื่อประหยัด $ 0.10 สำหรับต้นทุนฮาร์ดแวร์ต่อหน่วย
Bart van Ingen Schenau

2
@JohnWu: คุณทำให้การifตรวจสอบง่ายขึ้นแต่ลืมย้อนกลับผลลัพธ์ของการเปรียบเทียบ ฟังก์ชั่นของคุณคือตอนนี้กลับมาtrueเมื่อa + bเป็นไม่ได้อยู่ในช่วง เพิ่ม a !ไปที่ด้านนอกของเงื่อนไข ( return !(a > 1000 || a < -1000)), หรือกระจายการ!ทดสอบแบบ inverting เพื่อรับreturn a <= 1000 && a >= -1000;หรือทำให้การไหลของการตรวจสอบเป็นไปอย่างreturn -1000 <= a && a <= 1000;
ราบรื่น

1
@JohnWu: ยังคงปิดอยู่เล็กน้อยที่ขอบเคสตรรกะแบบกระจายต้องใช้<=/ >=ไม่ใช่</ >(ด้วย</ >, 1,000 และ -1000 ถูกถือว่าอยู่นอกช่วงรหัสต้นฉบับถือว่าเป็นเหมือนในช่วง)
ShadowRanger

11

ฉันจะปรับให้เหมาะสมสำหรับการอ่าน วิธีที่ 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);
}

วิธีการขนาดเล็กที่ทำสิ่งเดียว แต่ง่ายต่อการให้เหตุผล

(นี่คือการตั้งค่าส่วนตัวฉันชอบการทดสอบในเชิงบวกแทนการลบรหัสเดิมของคุณคือการทดสอบจริง ๆ ว่าค่าไม่ได้อยู่นอกช่วง)


5
นี้. (ความคิดเห็นที่ยกมาเหนือที่มีความคล้ายคลึงกันอีกครั้ง: ความสามารถในการอ่าน) 30 ปีที่แล้วเมื่อเราทำงานกับเครื่องที่มี RAM น้อยกว่า 1mb จำเป็นต้องมีประสิทธิภาพในการบีบ - เช่นเดียวกับปัญหา y2k รับสองสามแสนเรกคอร์ดที่แต่ละหน่วยมีหน่วยความจำไม่กี่ไบต์เนื่องจากสูญเสีย vars ที่ไม่ได้ใช้ การอ้างอิง ฯลฯ และจะเพิ่มขึ้นอย่างรวดเร็วเมื่อคุณมี RAM 256k เท่านั้น ตอนนี้เรากำลังจัดการกับเครื่องที่มี RAM หลายกิกะไบต์การประหยัดแม้แต่การใช้ RAM เพียงไม่กี่ MB เทียบกับความสามารถในการอ่านและการบำรุงรักษารหัสไม่ได้ดีนัก
ivanivan

@ivanivan: ฉันไม่คิดว่า "ปัญหา y2k" เป็นเรื่องเกี่ยวกับความทรงจำจริงๆ จากมุมมองการป้อนข้อมูลการป้อนตัวเลขสองหลักนั้นมีประสิทธิภาพมากกว่าการป้อนสี่หลักและการเก็บสิ่งต่าง ๆ ตามที่ป้อนนั้นง่ายกว่าการแปลงให้เป็นรูปแบบอื่น
supercat

10
ตอนนี้คุณต้องติดตามผ่าน 2 ฟังก์ชั่นเพื่อดูว่าเกิดอะไรขึ้น คุณไม่สามารถใช้ค่านี้ได้เนื่องจากคุณไม่สามารถบอกได้จากชื่อว่าสิ่งเหล่านี้มีความครอบคลุมหรือเฉพาะ และถ้าคุณเพิ่มข้อมูลนั้นชื่อของฟังก์ชันนั้นยาวเกินกว่ารหัสที่จะแสดง
ปีเตอร์

1
ปรับความสามารถในการอ่านให้เหมาะสมและสร้างฟังก์ชั่นขนาดเล็กและง่ายต่อการใช้งาน - แน่นอนยอมรับ แต่ผมเห็นด้วยอย่างยิ่งว่าการเปลี่ยนชื่อaและbไปnumber1และnumber2โรคเอดส์ให้สามารถอ่านได้ในทางใดทางหนึ่ง นอกจากนี้การตั้งชื่อฟังก์ชั่นของคุณก็ไม่สอดคล้องกัน: ทำไมฮาร์IsSumInRangeโค้ดในช่วงถ้าIsValueInRangeยอมรับว่าเป็นอาร์กิวเมนต์?
leftaroundabout

ฟังก์ชั่นที่ 1 สามารถล้น (เช่นเดียวกับรหัสคำตอบอื่น ๆ ) แม้ว่าความซับซ้อนของรหัสที่ปลอดภัยมากเกินไปก็เป็นข้อโต้แย้งในการใส่ลงในฟังก์ชั่น
philipxy

6

ในระยะสั้นฉันไม่คิดว่าคำถามมีความเกี่ยวข้องมากในการคำนวณปัจจุบัน แต่จากมุมมองทางประวัติศาสตร์มันเป็นความคิดที่น่าสนใจ

ผู้สัมภาษณ์ของคุณน่าจะเป็นแฟนตัวยงของ Mythical Man Month ในหนังสือ Fred Brooks ทำให้กรณีที่โปรแกรมเมอร์ต้องการฟังก์ชั่นหลักสองรุ่นในกล่องเครื่องมือของพวกเขา: รุ่นที่ปรับหน่วยความจำและรุ่นที่ปรับซีพียูให้เหมาะสม Fred อ้างอิงจากประสบการณ์ของเขาในการเป็นผู้นำการพัฒนาระบบปฏิบัติการ IBM System / 360 โดยที่เครื่องอาจมี RAM น้อยถึง 8 กิโลไบท์ ในเครื่องดังกล่าวหน่วยความจำที่จำเป็นสำหรับตัวแปรท้องถิ่นในฟังก์ชั่นอาจมีความสำคัญโดยเฉพาะอย่างยิ่งหากคอมไพเลอร์ไม่ได้เพิ่มประสิทธิภาพอย่างมีประสิทธิภาพออกไป (หรือถ้ารหัสถูกเขียนในภาษาแอสเซมบลีโดยตรง)

ในยุคปัจจุบันฉันคิดว่าคุณคงยากที่จะหาระบบที่การมีหรือไม่มีตัวแปรโลคอลในวิธีนั้นจะสร้างความแตกต่างที่เห็นได้ชัดเจน สำหรับตัวแปรที่มีความสำคัญวิธีการจะต้องมีการเรียกซ้ำด้วยการเรียกซ้ำที่ลึก แม้ว่าจะมีความเป็นไปได้สูงที่ความลึกของสแต็กจะทำให้เกิดข้อยกเว้นสแต็กโอเวอร์โฟลว์ก่อนที่ตัวแปรจะทำให้เกิดปัญหา สถานการณ์จริงเท่านั้นที่อาจมีปัญหาคือมีขนาดใหญ่มากอาร์เรย์ที่จัดสรรในกองซ้อนในวิธีการเรียกซ้ำ แต่นั่นก็ไม่น่าเป็นไปได้เพราะฉันคิดว่านักพัฒนาส่วนใหญ่จะคิดสองครั้งเกี่ยวกับชุดอาร์เรย์ขนาดใหญ่ที่ไม่จำเป็น


4

หลังจากการมอบหมาย s = a + b; ตัวแปร a และ b ไม่ได้ใช้อีกต่อไป ดังนั้นจึงไม่มีการใช้หน่วยความจำสำหรับ s หากคุณไม่ได้ใช้คอมไพเลอร์ที่เสียหายในสมองอย่างสมบูรณ์ หน่วยความจำที่ใช้อย่างไรก็ตามสำหรับ a และ b นั้นถูกนำมาใช้ใหม่

แต่การเพิ่มประสิทธิภาพฟังก์ชั่นนี้เป็นเรื่องไร้สาระที่สุด หากคุณสามารถประหยัดพื้นที่ได้อาจเป็น 8 ไบต์ในขณะที่ฟังก์ชันกำลังทำงาน (ซึ่งจะถูกกู้คืนเมื่อฟังก์ชันส่งคืน) ดังนั้นจะไม่มีจุดหมายอย่างแน่นอน ถ้าคุณสามารถประหยัดเวลามันจะเป็นตัวเลขนาโนวินาทีเดียว การเพิ่มประสิทธิภาพนี่เป็นการเสียเวลาโดยรวม


3

ตัวแปรประเภทค่าในตัวเครื่องได้รับการจัดสรรบนสแต็กหรือ (มีโอกาสมากขึ้นสำหรับรหัสชิ้นเล็ก ๆ ) ใช้รีจิสเตอร์ในโปรเซสเซอร์และไม่เคยเห็น RAM ใด ๆ ไม่ว่าพวกเขาจะอายุสั้นเพียงใดและไม่มีอะไรต้องกังวล คุณเริ่มพิจารณาการใช้หน่วยความจำเมื่อคุณต้องการบัฟเฟอร์หรือองค์ประกอบข้อมูลคิวในคอลเลกชันที่อาจมีขนาดใหญ่และยาว

จากนั้นขึ้นอยู่กับสิ่งที่คุณใส่ใจมากที่สุดสำหรับการสมัครของคุณ ความเร็วในการประมวลผล? เวลาตอบสนอง? หน่วยความจำรอยเท้า? การบำรุงรักษา? ความสอดคล้องในการออกแบบ? ทั้งหมดขึ้นอยู่กับคุณ.


4
Nitpicking:. NET อย่างน้อย (ภาษาของโพสต์ไม่ได้ระบุ) ไม่รับประกันใด ๆ เกี่ยวกับตัวแปรท้องถิ่นที่ได้รับการจัดสรร "ในสแต็ค" ดูที่"สแต็กคือรายละเอียดการนำไปใช้งาน"โดย Eric Lippert
jrh

1
@jrh ตัวแปรท้องถิ่นในกองหรือกองอาจจะเป็นรายละเอียดการดำเนินงาน แต่ถ้ามีคนอยากตัวแปรในกองมีและตอนนี้stackalloc Span<T>อาจมีประโยชน์ในจุดร้อนหลังจากทำโปรไฟล์ นอกจากนี้เอกสารบางส่วนรอบ ๆ structs แปลว่าชนิดของค่าอาจอยู่ในสแต็กในขณะที่ประเภทการอ้างอิงจะไม่เป็น อย่างไรก็ตามอย่างดีที่สุดคุณอาจหลีกเลี่ยง GC เล็กน้อย
บ๊อบ

2

ดังที่คำตอบอื่น ๆ ได้กล่าวไว้คุณต้องคิดในสิ่งที่คุณปรับให้เหมาะสม

ในตัวอย่างนี้ฉันสงสัยว่าคอมไพเลอร์ที่เหมาะสมจะสร้างโค้ดที่เทียบเท่าสำหรับทั้งสองวิธีดังนั้นการตัดสินใจจะไม่มีผลต่อเวลารันไทม์หรือหน่วยความจำ!

สิ่งที่มันไม่ส่งผลกระทบต่อการอ่านเป็นของรหัส (รหัสสำหรับมนุษย์ที่จะอ่านไม่ใช่แค่คอมพิวเตอร์) ไม่มีความแตกต่างระหว่างสองตัวอย่างมากเกินไป เมื่อทุกสิ่งเท่าเทียมกันฉันคิดว่าความกะทัดรัดเป็นคุณธรรมดังนั้นฉันจึงอาจเลือกวิธีที่ 2 แต่สิ่งอื่น ๆ ทั้งหมดนั้นไม่เท่าเทียมกันและในกรณีของโลกแห่งความซับซ้อนที่ซับซ้อนมากขึ้น

สิ่งที่ต้องพิจารณา:

  • นิพจน์กลางมีผลข้างเคียงหรือไม่? ถ้ามันเรียกฟังก์ชั่นที่ไม่บริสุทธิ์หรืออัพเดทตัวแปรใด ๆ แน่นอนว่าการทำซ้ำมันจะเป็นเรื่องของความถูกต้องไม่ใช่แค่สไตล์
  • การแสดงออกระดับกลางนั้นซับซ้อนแค่ไหน? หากมันมีฟังก์ชั่นการคำนวณและ / หรือการเรียกจำนวนมากคอมไพเลอร์อาจไม่สามารถปรับให้เหมาะสมและดังนั้นสิ่งนี้จะส่งผลกระทบต่อประสิทธิภาพ (แม้ว่า Knuth ดังที่กล่าวว่า “ เราควรลืมประสิทธิภาพเล็กน้อยให้พูดประมาณ 97% ของเวลา”)
  • ตัวแปรกลางมีความหมายหรือไม่? ขอชื่อที่ช่วยอธิบายสิ่งที่เกิดขึ้นได้ไหม ชื่อสั้น ๆ แต่ให้ข้อมูลสามารถอธิบายรหัสได้ดีขึ้นในขณะที่ชื่อที่ไม่มีความหมายนั้นเป็นเพียงเสียงรบกวนทางสายตา
  • นิพจน์กลางอยู่นานแค่ไหน หากนานเกินไปการทำซ้ำอาจทำให้โค้ดยาวขึ้นและอ่านยากขึ้น (โดยเฉพาะถ้าบังคับให้ตัวแบ่งบรรทัด); ถ้าไม่ทำซ้ำอาจจะสั้นกว่าทั้งหมด

1

ดังที่หลายคำตอบได้ชี้ให้เห็นการพยายามปรับฟังก์ชั่นนี้ด้วยคอมไพเลอร์สมัยใหม่จะไม่สร้างความแตกต่าง เครื่องมือเพิ่มประสิทธิภาพมีแนวโน้มที่จะหาทางออกที่ดีที่สุด (โหวตให้กับคำตอบที่แสดงรหัสแอสเซมเบลอร์เพื่อพิสูจน์!) คุณระบุว่ารหัสในการสัมภาษณ์นั้นไม่ตรงกับรหัสที่คุณขอให้เปรียบเทียบดังนั้นบางทีตัวอย่างจริงอาจสมเหตุสมผลกว่า

แต่ลองมาดูคำถามนี้กันอีก: นี่เป็นคำถามสัมภาษณ์ ดังนั้นปัญหาที่แท้จริงคือคุณควรตอบอย่างไรโดยสมมติว่าคุณต้องการลองรับงาน

สมมติว่าผู้สัมภาษณ์รู้ว่าพวกเขากำลังพูดถึงอะไรและพวกเขาแค่พยายามดูว่าคุณรู้อะไร

ฉันจะพูดถึงว่าการเพิกเฉยเครื่องมือเพิ่มประสิทธิภาพตัวแรกอาจสร้างตัวแปรชั่วคราวบนสแต็กในขณะที่ตัวที่สองไม่ทำ แต่จะทำการคำนวณสองครั้ง ดังนั้นหน่วยความจำแรกจึงใช้หน่วยความจำมากกว่า แต่เร็วกว่า

คุณสามารถพูดถึงได้ว่าการคำนวณอาจต้องการตัวแปรชั่วคราวเพื่อเก็บผลลัพธ์ (เพื่อให้สามารถเปรียบเทียบได้) ดังนั้นไม่ว่าคุณจะตั้งชื่อตัวแปรนั้นหรือไม่อาจสร้างความแตกต่าง

ฉันจะพูดถึงว่าในความเป็นจริงแล้วรหัสจะได้รับการปรับให้เหมาะสมและน่าจะเกิดจากรหัสเครื่องที่เทียบเท่าเนื่องจากตัวแปรทั้งหมดเป็นแบบท้องถิ่น อย่างไรก็ตามมันขึ้นอยู่กับคอมไพเลอร์ที่คุณใช้ (เมื่อไม่นานมานี้ว่าฉันจะได้รับการปรับปรุงประสิทธิภาพที่มีประโยชน์โดยการประกาศตัวแปรท้องถิ่นเป็น "final" ใน Java)

คุณสามารถพูดถึงว่าสแต็คในกรณีใด ๆ ที่อาศัยอยู่ในหน้าหน่วยความจำของตัวเองดังนั้นหากตัวแปรพิเศษของคุณทำให้กองซ้อนที่จะล้นหน้าในความเป็นจริงมันจะไม่จัดสรรหน่วยความจำอีกต่อไป ถ้ามันล้นมันจะต้องการหน้าใหม่ทั้ง

ฉันจะพูดถึงว่าตัวอย่างที่เป็นจริงมากขึ้นอาจเป็นทางเลือกว่าจะใช้แคชเพื่อเก็บผลลัพธ์ของการคำนวณจำนวนมากหรือไม่และสิ่งนี้จะทำให้เกิดคำถามของซีพียูกับหน่วยความจำ

ทั้งหมดนี้แสดงให้เห็นว่าคุณรู้ว่าคุณกำลังพูดถึงอะไร

ฉันจะปล่อยให้มันจบเพื่อบอกว่ามันจะเป็นการดีกว่าที่จะมุ่งเน้นไปที่ความพร้อมอ่านแทน แม้ว่าจริงในกรณีนี้ในบริบทการสัมภาษณ์มันอาจตีความว่าเป็น "ฉันไม่รู้เกี่ยวกับประสิทธิภาพการทำงาน แต่รหัสของฉันอ่านเหมือนเรื่องราวของเจเน็ตและจอห์น "

สิ่งที่คุณไม่ควรทำคือเดินตามคำสั่งธรรมดา ๆ เกี่ยวกับวิธีการปรับแต่งโค้ดไม่จำเป็นอย่าปรับให้เหมาะสมจนกว่าคุณจะทำโค้ดให้เรียบร้อย (นี่เป็นการระบุว่าคุณไม่เห็นโค้ดที่ไม่ดีสำหรับตัวเอง) และได้โปรดได้โปรดอย่าพูดคำว่า Knuth "premature blah blah ... "

ประสิทธิภาพของรหัสเป็นปัญหาของแท้ในหลาย ๆ องค์กรและหลายองค์กรต้องการโปรแกรมเมอร์ที่เข้าใจ

โดยเฉพาะอย่างยิ่งกับองค์กรต่าง ๆ เช่น Amazon รหัสบางส่วนมีประโยชน์มากมาย ข้อมูลโค้ดอาจถูกปรับใช้บนเซิร์ฟเวอร์หลายพันเครื่องหรืออุปกรณ์นับล้านเครื่องและอาจเรียกได้ว่าเป็นพันล้านครั้งต่อวันทุกวันของปี อาจมีตัวอย่างที่คล้ายกันหลายพันรายการ ความแตกต่างระหว่างอัลกอริธึมที่ไม่ดีและอันที่ดีอาจเป็นปัจจัยหนึ่งพันได้อย่างง่ายดาย ทำตัวเลขและทวีคูณทั้งหมด: มันสร้างความแตกต่าง ค่าใช้จ่ายที่อาจเกิดขึ้นกับองค์กรของโค้ดที่ไม่ทำงานอาจมีความสำคัญมากหรืออาจถึงขั้นเสียชีวิตหากระบบมีความจุไม่เพียงพอ

Furthmore องค์กรเหล่านี้ทำงานในสภาพแวดล้อมที่มีการแข่งขันสูง ดังนั้นคุณไม่สามารถบอกให้ลูกค้าซื้อคอมพิวเตอร์ที่ใหญ่กว่าได้หากซอฟต์แวร์ของคู่แข่งของคุณใช้งานได้ดีกับฮาร์ดแวร์ที่มีอยู่หรือหากซอฟต์แวร์ทำงานบนโทรศัพท์มือถือและไม่สามารถอัพเกรดได้ แอปพลิเคชั่นบางตัวนั้นมีความสำคัญอย่างยิ่งต่อประสิทธิภาพ (เกมและแอพมือถือเป็นสิ่งสำคัญ) และอาจมีชีวิตอยู่หรือตายไปตามการตอบสนองหรือความเร็ว

ฉันทำงานเป็นการส่วนตัวมานานกว่าสองทศวรรษในหลายโครงการที่ระบบล้มเหลวหรือไม่สามารถใช้งานได้เนื่องจากปัญหาด้านประสิทธิภาพและฉันถูกเรียกใช้ในการเพิ่มประสิทธิภาพของระบบเหล่านั้นและในทุกกรณีมันเกิดจากรหัสไม่ดีที่เขียนโดยโปรแกรมเมอร์ที่ไม่เข้าใจ ผลกระทบของสิ่งที่พวกเขาเขียน ยิ่งไปกว่านั้นมันไม่เคยเป็นรหัสชิ้นเดียวทุกที่ทุกเวลา เมื่อฉันปรากฏมันเป็นวิธีที่จะเริ่มคิดเกี่ยวกับประสิทธิภาพ: ความเสียหายได้รับการทำ

การทำความเข้าใจเกี่ยวกับประสิทธิภาพของรหัสเป็นทักษะที่ดีที่มีเช่นเดียวกับการเข้าใจความถูกต้องของรหัสและลักษณะของรหัส มันมาจากการปฏิบัติ ความล้มเหลวด้านประสิทธิภาพอาจไม่ดีเท่าความล้มเหลวในการทำงาน หากระบบไม่ทำงานจะไม่ทำงาน ไม่สำคัญว่าทำไม ในทำนองเดียวกันประสิทธิภาพและคุณสมบัติที่ไม่เคยใช้ก็ไม่ดีเช่นกัน

ดังนั้นหากผู้สัมภาษณ์ถามคุณเกี่ยวกับการแสดงผมขอแนะนำให้ลองแสดงความรู้ให้มากที่สุด หากคำถามดูเหมือนว่าไม่ดีให้ชี้อย่างสุภาพว่าทำไมคุณถึงคิดว่ามันจะไม่เป็นปัญหาในกรณีนั้น อย่าพูดคำว่า Knuth


0

คุณควรปรับให้เหมาะสมเพื่อความถูกต้องก่อน

ฟังก์ชันของคุณล้มเหลวสำหรับค่าอินพุตที่อยู่ใกล้กับ Int.MaxValue:

int a = int.MaxValue - 200;
int b = int.MaxValue - 200;
bool inRange = test.IsSumInRangeA(a, b);

สิ่งนี้จะคืนค่าจริงเนื่องจากผลรวมล้นไปถึง -400 ฟังก์ชั่นนี้ใช้งานไม่ได้กับ a = int.MinValue + 200 (เพิ่มอย่างไม่ถูกต้องถึง "400")

เราจะไม่ทราบว่าสิ่งที่ผู้สัมภาษณ์กำลังมองหาถ้าเขาหรือเธอตีระฆังใน แต่"ล้นเป็นจริง"

ในสถานการณ์การสัมภาษณ์ถามคำถามเพื่อชี้แจงขอบเขตของปัญหา: อะไรคือค่าสูงสุดและต่ำสุดที่อนุญาตคืออะไร เมื่อคุณมีแล้วคุณสามารถโยนข้อยกเว้นถ้าผู้โทรส่งค่านอกช่วง หรือ (ใน C #) คุณสามารถใช้ส่วน {} ที่ทำเครื่องหมายไว้ซึ่งจะทำให้เกิดข้อยกเว้นเกี่ยวกับโอเวอร์โฟลว์ ใช่มันทำงานและซับซ้อนมากขึ้น แต่บางครั้งก็เป็นสิ่งที่ต้องทำ


วิธีการเป็นเพียงตัวอย่าง พวกเขาไม่ได้เขียนให้ถูกต้อง แต่เพื่อแสดงคำถามจริง ขอบคุณสำหรับการป้อนข้อมูลแม้ว่า!
Corey P

ฉันคิดว่าคำถามสัมภาษณ์นั้นมุ่งไปที่การแสดงดังนั้นคุณต้องตอบเจตนาของคำถาม ผู้สัมภาษณ์ไม่ถามเกี่ยวกับพฤติกรรมตามขีด จำกัด แต่ประเด็นที่น่าสนใจอยู่ดี
rghome

1
@Corey ผู้สัมภาษณ์ที่ดีเป็นคำถามต่อ 1) ประเมินความสามารถของผู้สมัครที่เกี่ยวข้องกับปัญหาตามที่ rghome แนะนำที่นี่และ 2) เป็นการเปิดประเด็นที่ใหญ่กว่า (เช่นความถูกต้องของหน้าที่ไม่ได้พูด) และความรู้ที่เกี่ยวข้อง ในการสัมภาษณ์อาชีพในภายหลัง - ขอให้โชคดี
chux

0

คำถามของคุณควรเป็น: "ฉันต้องการเพิ่มประสิทธิภาพนี้หรือไม่?"

เวอร์ชัน A และ B แตกต่างกันในรายละเอียดสำคัญที่ทำให้ A preferrable แต่ไม่เกี่ยวข้องกับการปรับให้เหมาะสม: คุณไม่ต้องทำซ้ำรหัส

"การปรับให้เหมาะสม" ที่แท้จริงเรียกว่าการกำจัด subexpression ทั่วไปซึ่งเป็นสิ่งที่คอมไพเลอร์ทุกคนทำได้ บางคนทำเช่นนี้การเพิ่มประสิทธิภาพขั้นพื้นฐานแม้ว่าจะปิดการเพิ่มประสิทธิภาพ เพื่อที่จะไม่เป็นการเพิ่มประสิทธิภาพอย่างแท้จริง (รหัสที่สร้างขึ้นจะเกือบเหมือนกันในทุกกรณี)

แต่ถ้ามันไม่ใช่การเพิ่มประสิทธิภาพทำไมมันถึงดีกว่า? เอาล่ะคุณไม่ต้องทำซ้ำรหัสใครสนใจ!

ก่อนอื่นคุณไม่มีความเสี่ยงที่จะได้รับครึ่งหนึ่งของเงื่อนไขข้อผิดพลาดโดยไม่ตั้งใจ แต่ที่สำคัญกว่านั้นคือใครบางคนที่อ่านรหัสนี้สามารถคร่ำครวญทันทีว่าคุณกำลังทำอะไรอยู่แทนที่จะเป็นif((((wtf||is||this||longexpression))))ประสบการณ์ สิ่งที่ผู้อ่านได้เห็นคือif(one || theother)สิ่งที่ดี ไม่น้อยฉันเกิดขึ้นว่าคุณเป็นคนที่อ่านโค้ดของคุณเองในอีกสามปีต่อมาและคิดว่า "WTF หมายความว่าอย่างไร" ในกรณีดังกล่าวจะเป็นประโยชน์เสมอหากรหัสของคุณสื่อสารได้ทันทีว่าเจตนาคืออะไร ด้วย subexpression ทั่วไปถูกตั้งชื่ออย่างถูกต้องเป็นกรณี
นอกจากนี้ถ้าในเวลาใด ๆ ในอนาคตคุณตัดสินใจว่าเช่นคุณจำเป็นต้องเปลี่ยนa+bไปa-bคุณจะมีการเปลี่ยนแปลงอย่างใดอย่างหนึ่งสถานที่ไม่ใช่สองแห่ง และไม่มีความเสี่ยงที่จะเกิดความผิดพลาดครั้งที่สองโดยบังเอิญ

เกี่ยวกับคำถามที่เกิดขึ้นจริงของคุณสิ่งที่คุณควรเพิ่มประสิทธิภาพสำหรับครั้งแรกของทุกรหัสของคุณควรจะถูกต้อง นี่คือสิ่งที่สำคัญที่สุดอย่างแน่นอน รหัสที่ไม่ถูกต้องคือรหัสที่ไม่ถูกต้องแม้จะเป็นรหัสที่ไม่ถูกต้องก็ตาม "ทำงานได้ดี" หรืออย่างน้อยก็ดูเหมือนว่าจะทำงานได้ดี หลังจากนั้นรหัสควรอ่านได้ (มีคนที่ไม่คุ้นเคยอ่านได้)
สำหรับการปรับให้เหมาะสม ... หนึ่งไม่ควรเขียนโค้ดต่อต้านการปรับให้เหมาะสมและแน่นอนฉันไม่ได้บอกว่าคุณไม่ควรใช้ความคิดในการออกแบบก่อนที่จะเริ่ม (เช่นการเลือกอัลกอริทึมที่เหมาะสมสำหรับปัญหา ไม่มีประสิทธิภาพน้อยที่สุด)

แต่สำหรับแอพพลิเคชั่นส่วนใหญ่ส่วนใหญ่แล้วประสิทธิภาพที่คุณได้รับหลังจากรันโค้ดที่ถูกต้องและสามารถอ่านได้โดยใช้อัลกอริธึมที่เหมาะสมผ่านคอมไพเลอร์ที่ปรับให้เหมาะสมนั้นใช้ได้ดีมาก

หากไม่เป็นเช่นนั้นหากประสิทธิภาพของแอปพลิเคชั่นนั้นไม่ตรงตามข้อกำหนดและคุณควรกังวลเกี่ยวกับการปรับแต่งเฉพาะที่อย่างที่คุณต้องการ อย่างไรก็ตามโดยเฉพาะอย่างยิ่งคุณจะต้องพิจารณาอัลกอริทึมระดับบนสุดอีกครั้ง หากคุณเรียกใช้ฟังก์ชัน 500 ครั้งแทนที่จะเป็น 50,000 ครั้งเนื่องจากอัลกอริทึมที่ดีกว่านี้จะมีผลกระทบมากกว่าการบันทึกวงจรนาฬิกาสามรอบในการปรับให้เหมาะสมแบบไมโคร หากคุณไม่ปิดกั้นหลายร้อยรอบในการเข้าถึงหน่วยความจำแบบสุ่มตลอดเวลาสิ่งนี้มีผลกระทบที่ใหญ่กว่าการทำการคำนวณราคาถูกพิเศษอื่น ๆ ฯลฯ

การเพิ่มประสิทธิภาพเป็นเรื่องยาก (คุณสามารถเขียนหนังสือทั้งเล่มเกี่ยวกับเรื่องนี้และไม่สิ้นสุด) และใช้เวลาในการเพิ่มประสิทธิภาพบางจุด (โดยไม่รู้ตัวว่าเป็นปัญหาคอขวดเลย!) มักจะเสียเวลา การปรับให้เหมาะสมนั้นทำได้ยากมาก

แต่ตามกฎทั่วไปเมื่อคุณตาบอดและเพียงต้องการ / ต้องการทำอะไรบางอย่างหรือเป็นกลยุทธ์เริ่มต้นทั่วไปฉันขอแนะนำให้ปรับให้เหมาะสมสำหรับ "หน่วยความจำ"
การปรับให้เหมาะสมสำหรับ "หน่วยความจำ" (โดยเฉพาะพื้นที่เชิงพื้นที่และรูปแบบการเข้าถึง) มักจะให้ประโยชน์เพราะไม่เหมือนกาลครั้งหนึ่งเมื่อทุกอย่างเป็น "kinda เดียวกัน" ทุกวันนี้การเข้าถึงแรมเป็นหนึ่งในสิ่งที่แพงที่สุด โดยหลักการแล้วคุณสามารถทำได้ ในขณะที่ ALU กลับมีราคาถูกและเร็วขึ้นทุกสัปดาห์ แบนด์วิดท์หน่วยความจำและเวลาแฝงไม่ดีขึ้นอย่างรวดเร็ว สถานที่ที่ดีและรูปแบบการเข้าถึงที่ดีสามารถสร้างความแตกต่างได้อย่างง่ายดาย 5x (20x ในตัวอย่างที่ผิดเพี้ยนไปอย่างมาก) ในขณะทำงานเมื่อเทียบกับรูปแบบการเข้าถึงที่ไม่ดีในแอปพลิเคชันที่มีข้อมูลจำนวนมาก เป็นคนดีกับแคชของคุณและคุณจะเป็นคนที่มีความสุข

ในการทำให้ย่อหน้าก่อนหน้าเป็นมุมมองพิจารณาสิ่งต่าง ๆ ที่คุณสามารถทำให้คุณเสียค่าใช้จ่าย การดำเนินการบางอย่างเช่นa+bใช้เวลา (ถ้าไม่ปรับให้เหมาะสม) หนึ่งหรือสองรอบ แต่โดยปกติแล้วซีพียูสามารถเริ่มต้นคำสั่งได้หลายคำสั่งต่อหนึ่งรอบ โดยหลักการแล้วหากคอมไพเลอร์เก่งในการจัดตารางเวลาและขึ้นอยู่กับสถานการณ์อาจมีค่าใช้จ่ายเป็นศูนย์
การดึงข้อมูล ("หน่วยความจำ") ทำให้คุณเสียค่าใช้จ่าย 4-5 รอบหากคุณโชคดีและอยู่ใน L1 และรอบ 15 รอบหากคุณไม่โชคดี (L2 เข้าชม) หากข้อมูลไม่ได้อยู่ในแคชเลยจะใช้เวลาหลายร้อยรอบ หากรูปแบบการเข้าถึงแบบจับจดของคุณนั้นเกินขีดความสามารถของ TLB (ง่ายต่อการทำกับรายการเพียง ~ 50 รายการ) เพิ่มอีกสองสามร้อยรอบ หากรูปแบบการเข้าถึงแบบจับจดของคุณทำให้เกิดความผิดพลาดของหน้าเว็บจริงคุณต้องเสียค่าใช้จ่ายสองสามหมื่นครั้งในกรณีที่ดีที่สุด
ลองคิดดูสิสิ่งใดที่คุณต้องการหลีกเลี่ยงอย่างเร่งด่วนที่สุด?


0

เมื่อใดจึงควรปรับให้เหมาะสมสำหรับหน่วยความจำและประสิทธิภาพความเร็วสำหรับวิธีการ?

หลังจากได้รับการทำงานที่เหมาะสมแรก จากนั้นหัวกะทิเกี่ยวข้องกับตัวเองด้วยการเพิ่มประสิทธิภาพขนาดเล็ก


ในฐานะที่เป็นคำถามสัมภาษณ์เกี่ยวกับการปรับให้เหมาะสมรหัสจะกระตุ้นการสนทนาตามปกติ แต่พลาดเป้าหมายระดับที่สูงขึ้นของรหัสนั้นถูกต้องตามหน้าที่หรือไม่

ทั้ง C ++ และ C และอื่น ๆ ถือว่าล้นเป็นปัญหาจากการที่int a + bมันไม่ได้กำหนดไว้อย่างดีและ C เรียกมันว่าพฤติกรรมที่ไม่ได้กำหนด ไม่ได้ระบุว่า "wrap" - แม้ว่าจะเป็นพฤติกรรมทั่วไป

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

เช่นฟังก์ชั่นที่เรียกว่าIsSumInRange()จะได้รับการคาดหวังว่าจะได้รับการกำหนดไว้อย่างดีและดำเนินการอย่างถูกต้องสำหรับทุกค่าของint a,bดิบa + bไม่ได้ โซลูชัน AC สามารถใช้:

#define N 1000
bool IsSumInRange_FullRange(int a, int b) {
  if (a >= 0) {
    if (b > INT_MAX - a) return false;
  } else {
    if (b < INT_MIN - a) return false;
  }
  int sum = a + b;
  if (sum > N || sum < -N) return false;
  else return true;
}

รหัสดังกล่าวจะได้รับการปรับให้เหมาะสมโดยใช้ชนิดจำนวนเต็มกว้างกว่าintหากมีดังต่อไปนี้หรือกระจายsum > N, sum < -Nการทดสอบภายในif (a >= 0)ตรรกะ แต่การเพิ่มประสิทธิภาพดังกล่าวอาจไม่นำไปสู่รหัสที่ "ปล่อย" ได้เร็วขึ้นเนื่องจากคอมไพเลอร์ที่ชาญฉลาดและไม่คุ้มค่ากับการบำรุงรักษาที่ชาญฉลาด

  long long sum a;
  sum += b;

แม้ใช้มีแนวโน้มที่จะเกิดปัญหาเมื่อabs(sum)sum == INT_MIN


0

เรากำลังพูดถึงคอมไพเลอร์ประเภทใดและ "ความทรงจำ" ประเภทใด? เนื่องจากในตัวอย่างของคุณสมมติว่าเครื่องมือเพิ่มประสิทธิภาพที่เหมาะสมนิพจน์a+bจะต้องเก็บไว้ในรีจิสเตอร์ (รูปแบบของหน่วยความจำ) ก่อนที่จะทำการคำนวณทางคณิตศาสตร์

ดังนั้นถ้าเรากำลังพูดถึงคอมไพเลอร์ใบ้ที่พบa+bสองครั้งมันจะจัดสรรรีจิสเตอร์ (หน่วยความจำ) มากขึ้นในตัวอย่างที่สองของคุณเพราะตัวอย่างแรกของคุณอาจเก็บนิพจน์นั้นเพียงครั้งเดียวในรีจิสเตอร์เดียวที่แมปกับตัวแปรท้องถิ่น เรากำลังพูดถึงคอมไพเลอร์ที่โง่มากในตอนนี้ ... เว้นแต่คุณกำลังทำงานกับคอมไพเลอร์โง่ ๆ ประเภทอื่นที่สแต็คกระจายตัวแปรทุกตัวไปทั่วสถานที่ซึ่งในกรณีนี้อาจจะเป็นครั้งแรกที่ทำให้ความเศร้าโศกเพิ่มประสิทธิภาพมากกว่า ที่สอง*.

ฉันยังคงต้องการที่จะมีรอยขีดข่วนที่และคิดว่าคนที่สองมีแนวโน้มที่จะใช้หน่วยความจำมากขึ้นด้วยคอมไพเลอร์ใบ้แม้จะมีแนวโน้มที่จะสแต็ครั่วไหลเพราะมันอาจจบลงด้วยการจัดสรรสามลงทะเบียนสำหรับการa+bและการรั่วไหลaและbอื่น ๆ อีกมากมาย ถ้าเรากำลังพูดถึงการเพิ่มประสิทธิภาพดั้งเดิมที่สุดแล้วจับa+bไปsอาจจะ "ช่วย" มันใช้ลงทะเบียนน้อย / การรั่วไหลของสแต็ค

ทั้งหมดนี้เป็นการเก็งกำไรอย่างมากในวิธีที่ค่อนข้างโง่ที่ขาดการวัด / ถอดแยกชิ้นส่วนและแม้ในสถานการณ์ที่เลวร้ายที่สุดกรณีนี้ไม่ใช่กรณี "หน่วยความจำกับประสิทธิภาพ" (เพราะแม้ในกลุ่มเครื่องมือเพิ่มประสิทธิภาพที่เลวร้ายที่สุดที่ฉันคิด เกี่ยวกับอะไรนอกจากหน่วยความจำชั่วคราวเช่น stack / register) มันเป็นกรณี "ประสิทธิภาพ" ที่ดีที่สุดและในบรรดาเครื่องมือเพิ่มประสิทธิภาพที่สมเหตุสมผลทั้งสองนั้นเทียบเท่ากันและหากใครไม่ได้ใช้เครื่องมือเพิ่มประสิทธิภาพที่เหมาะสมทำไมต้องกังวลเกี่ยวกับการเพิ่มประสิทธิภาพด้วยกล้องจุลทรรศน์ วัดขาดโดยเฉพาะอย่างยิ่ง? นั่นเป็นเหมือนการเลือกคำสั่ง / การโฟกัสระดับแอสเซมบลีการจัดสรรซึ่งฉันไม่เคยคาดหวังว่าทุกคนที่ต้องการรักษาประสิทธิภาพในการใช้พูดล่ามที่สแต็คทำทุกอย่าง

เมื่อใดจึงควรปรับให้เหมาะสมสำหรับหน่วยความจำและประสิทธิภาพความเร็วสำหรับวิธีการ?

สำหรับคำถามนี้หากฉันสามารถจัดการกับมันได้ในวงกว้างมากขึ้นฉันมักจะไม่พบสิ่งที่ตรงกันข้ามกับทั้งสอง โดยเฉพาะอย่างยิ่งหากรูปแบบการเข้าถึงของคุณเรียงตามลำดับและกำหนดความเร็วของแคช CPU บ่อยครั้งที่การลดจำนวนไบต์ที่ประมวลผลตามลำดับสำหรับอินพุตที่ไม่สำคัญแปลเป็นการแปล (จนถึงจุด) เพื่อไถลผ่านข้อมูลนั้นเร็วขึ้น แน่นอนว่ามีจุดแตกหักหากข้อมูลมีขนาดเล็กกว่ามากแลกเปลี่ยนกับวิธีคำแนะนำมากขึ้นอาจจะเร็วกว่าในการประมวลผลตามลำดับในรูปแบบที่มีขนาดใหญ่ขึ้นเพื่อแลกเปลี่ยนกับคำแนะนำที่น้อยลง

แต่ฉันพบว่า devs จำนวนมากมักจะดูถูกดูแคลนว่าการใช้หน่วยความจำลดลงมากน้อยเพียงใดในกรณีเหล่านี้สามารถแปลเป็นการลดสัดส่วนตามเวลาที่ใช้ในการประมวลผล มันง่ายมากที่จะแปลค่าใช้จ่ายด้านประสิทธิภาพเป็นคำแนะนำแทนที่จะเข้าถึงหน่วยความจำจนถึงจุดที่เข้าถึง LUT ขนาดใหญ่ในความพยายามที่ไร้ประโยชน์เพื่อเร่งการคำนวณขนาดเล็กเพียงเพื่อค้นหาประสิทธิภาพที่ลดลงด้วยการเข้าถึงหน่วยความจำเพิ่มเติม

สำหรับกรณีการเข้าถึงแบบลำดับต่อเนื่องผ่านอาร์เรย์ขนาดใหญ่ (ไม่พูดถึงตัวแปรสเกลาร์โลคัลเช่นในตัวอย่างของคุณ) ฉันไปตามกฎที่หน่วยความจำน้อยกว่าในการไถตามลำดับผ่านการแปลเพื่อประสิทธิภาพที่ดีขึ้นโดยเฉพาะอย่างยิ่งเมื่อรหัสผลลัพธ์นั้นง่ายกว่าอย่างอื่น จนกว่าการวัดและผู้สร้างโปรไฟล์ของฉันจะบอกฉันเป็นอย่างอื่นและมันก็สำคัญในทางเดียวกันฉันถือว่าการอ่านไฟล์ไบนารีที่มีขนาดเล็กลงบนดิสก์ตามลำดับนั้นจะเร็วกว่าการไถผ่านไฟล์ที่ใหญ่กว่า (แม้ว่าไฟล์ขนาดเล็กต้องการคำแนะนำเพิ่มเติม ) จนกระทั่งข้อสันนิษฐานนั้นแสดงว่าไม่มีผลบังคับใช้กับการวัดของฉันอีกต่อไป

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.