Khi nào cần tối ưu hóa cho bộ nhớ so với tốc độ hiệu năng cho một phương thức?


107

Gần đây tôi đã phỏng vấn tại Amazon. Trong một phiên mã hóa, người phỏng vấn hỏi tại sao tôi khai báo một biến trong một phương thức. Tôi đã giải thích quá trình của mình và anh ấy đã thách thức tôi giải quyết vấn đề tương tự với ít biến số hơn. Ví dụ (đây không phải là từ cuộc phỏng vấn), tôi đã bắt đầu với Phương pháp A sau đó cải thiện nó thành Phương pháp B, bằng cách xóa int s. Ông hài lòng và nói rằng điều này sẽ làm giảm việc sử dụng bộ nhớ bằng phương pháp này.

Tôi hiểu logic đằng sau nó, nhưng câu hỏi của tôi là:

Khi nào thì thích hợp để sử dụng Phương pháp A so với Phương pháp B và ngược lại?

Bạn có thể thấy rằng Phương thức A sẽ có mức sử dụng bộ nhớ cao hơn, vì int sđược khai báo, nhưng nó chỉ phải thực hiện một phép tính, tức là a + b. Mặt khác, Phương pháp B có mức sử dụng bộ nhớ thấp hơn, nhưng phải thực hiện hai phép tính, tức là a + bhai lần. Khi nào tôi sử dụng một kỹ thuật khác? Hoặc, là một trong những kỹ thuật luôn được ưa thích hơn các kỹ thuật khác? Những điều cần xem xét khi đánh giá hai phương pháp là gì?

Phương pháp A:

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

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

Phương pháp B:

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

229
Tôi sẵn sàng đặt cược rằng một trình biên dịch hiện đại sẽ tạo ra cùng một hội đồng cho cả hai trường hợp đó.
17 của 26

12
Tôi đã trả lại câu hỏi về trạng thái ban đầu, vì chỉnh sửa của bạn làm mất hiệu lực câu trả lời của tôi - xin đừng làm vậy! Nếu bạn hỏi một câu hỏi làm thế nào để cải thiện mã của bạn, thì đừng thay đổi câu hỏi bằng cách cải thiện mã theo cách hiển thị - điều này làm cho câu trả lời trông vô nghĩa.
Doc Brown

76
Đợi một chút, họ yêu cầu thoát khỏi int strong khi hoàn toàn ổn với những con số ma thuật cho giới hạn trên và dưới?
null

34
Ghi nhớ: hồ sơ trước khi tối ưu hóa. Với các trình biên dịch hiện đại, Phương pháp A và Phương pháp B có thể được tối ưu hóa cho cùng một mã (sử dụng các mức tối ưu hóa cao hơn). Ngoài ra, với các bộ xử lý hiện đại, họ có thể có các hướng dẫn thực hiện nhiều hơn ngoài một thao tác.
Thomas Matthews

142
Cũng không; tối ưu hóa cho dễ đọc.
Andy

Câu trả lời:


148

Thay vì suy đoán về những gì có thể hoặc không thể xảy ra, chúng ta hãy xem xét, phải không? Tôi sẽ phải sử dụng C ++ vì tôi không có trình biên dịch C # tiện dụng (mặc dù xem ví dụ C # từ VisualMelon ), nhưng tôi chắc chắn các nguyên tắc tương tự được áp dụng bất kể.

Chúng tôi sẽ bao gồm hai lựa chọn bạn gặp trong cuộc phỏng vấn. Chúng tôi cũng sẽ bao gồm một phiên bản sử dụng abstheo đề xuất của một số câu trả lời.

#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);
}

Bây giờ biên dịch nó mà không cần tối ưu hóa gì: g++ -c -o test.o test.cpp

Bây giờ chúng ta có thể thấy chính xác những gì nó tạo ra: 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

Chúng ta có thể thấy từ các địa chỉ ngăn xếp (ví dụ: -0x4in mov %edi,-0x4(%rbp)so với -0x14in mov %edi,-0x14(%rbp)) IsSumInRangeWithVar()sử dụng 16 byte bổ sung trên ngăn xếp.

Bởi vì IsSumInRangeWithoutVar()phân bổ không có không gian trên ngăn xếp để lưu trữ giá trị trung gian, snó phải tính toán lại nó, dẫn đến việc thực hiện này là 2 hướng dẫn lâu hơn.

Hài hước, IsSumInRangeSuperOptimized()trông rất giống IsSumInRangeWithoutVar(), ngoại trừ so với -1000 đầu tiên và 1000 giây.

Bây giờ hãy biên dịch chỉ với các tối ưu hóa cơ bản nhất : g++ -O1 -c -o test.o test.cpp. Kết quả:

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

Bạn sẽ nhìn vào đó: mỗi biến thể là giống hệt nhau . Trình biên dịch có thể làm một cái gì đó khá thông minh: abs(a + b) <= 1000tương đương với a + b + 1000 <= 2000việc xem xét setbethực hiện một so sánh không dấu, vì vậy một số âm trở thành một số dương rất lớn. Các leahướng dẫn thực sự có thể thực hiện tất cả những bổ sung trong một hướng dẫn, và loại bỏ tất cả các chi nhánh có điều kiện.

Để trả lời câu hỏi của bạn, hầu như luôn luôn điều tối ưu hóa không phải là bộ nhớ hay tốc độ, mà là khả năng đọc . Đọc mã khó hơn rất nhiều so với viết mã và đọc mã được đọc sai để "tối ưu hóa" khó hơn rất nhiều so với đọc mã được viết rõ ràng. Thường xuyên hơn không, những "tối ưu hóa" này không đáng kể, hoặc trong trường hợp này chính xác là không có tác động thực sự đến hiệu suất.


Theo dõi câu hỏi, những gì thay đổi khi mã này trong một ngôn ngữ diễn giải thay vì biên dịch? Sau đó, việc tối ưu hóa có vấn đề hay nó có kết quả tương tự?

Hãy đo! Tôi đã sao chép các ví dụ sang 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)),))

Chạy với Python 3.5.2, điều này tạo ra đầu ra:

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

Việc tháo gỡ trong Python không thú vị lắm, vì "trình biên dịch" mã byte không làm được gì nhiều trong cách tối ưu hóa.

Hiệu suất của ba chức năng gần như giống hệt nhau. Chúng tôi có thể bị cám dỗ để đi với IsSumInRangeWithVar()do tăng tốc độ cận biên của nó. Mặc dù tôi sẽ thêm khi tôi đang thử các tham số khác nhau timeit, đôi khi IsSumInRangeSuperOptimized()xuất hiện nhanh nhất, vì vậy tôi nghi ngờ đó có thể là yếu tố bên ngoài chịu trách nhiệm cho sự khác biệt, thay vì bất kỳ lợi thế nội tại nào của việc triển khai.

Nếu đây thực sự là mã hiệu suất quan trọng, một ngôn ngữ được hiểu đơn giản là một lựa chọn rất kém. Chạy cùng một chương trình với pypy, tôi nhận được:

IsSumInRangeWithVar: 0.000180s
IsSumInRangeWithoutVar: 0.001175s
IsSumInRangeSuperOptimized: 0.001306s

Chỉ cần sử dụng pypy, sử dụng trình biên dịch JIT để loại bỏ rất nhiều chi phí phiên dịch, đã mang lại sự cải thiện hiệu suất của 1 hoặc 2 bậc độ lớn. Tôi đã khá sốc khi thấy IsSumInRangeWithVar()một thứ tự cường độ nhanh hơn những thứ khác. Vì vậy, tôi đã thay đổi thứ tự của điểm chuẩn và chạy lại:

IsSumInRangeSuperOptimized: 0.000191s
IsSumInRangeWithoutVar: 0.001174s
IsSumInRangeWithVar: 0.001265s

Vì vậy, có vẻ như nó không thực sự là bất cứ điều gì về việc thực hiện nhanh chóng, mà là thứ tự tôi thực hiện điểm chuẩn!

Tôi muốn tìm hiểu sâu hơn về vấn đề này, vì thật lòng tôi không biết tại sao điều này lại xảy ra. Nhưng tôi tin rằng vấn đề đã được thực hiện: tối ưu hóa vi mô như có nên khai báo một giá trị trung gian là một biến hay không hiếm khi có liên quan. Với một ngôn ngữ được giải thích hoặc trình biên dịch được tối ưu hóa cao, mục tiêu đầu tiên vẫn là viết mã rõ ràng.

Nếu tối ưu hóa hơn nữa có thể được yêu cầu, điểm chuẩn . Hãy nhớ rằng các tối ưu hóa tốt nhất không đến từ các chi tiết nhỏ mà là bức tranh thuật toán lớn hơn: pypy sẽ là một thứ tự cường độ nhanh hơn để đánh giá lặp lại cùng chức năng so với cpython vì nó sử dụng thuật toán nhanh hơn (trình biên dịch JIT so với giải thích) để đánh giá chương trình. Và cũng có thuật toán được mã hóa để xem xét: tìm kiếm thông qua cây B sẽ nhanh hơn danh sách được liên kết.

Sau khi đảm bảo bạn đang sử dụng các công cụ và thuật toán phù hợp cho công việc, hãy chuẩn bị để đi sâu vào chi tiết của hệ thống. Kết quả có thể rất đáng ngạc nhiên, ngay cả đối với các nhà phát triển có kinh nghiệm và đây là lý do tại sao bạn phải có một điểm chuẩn để định lượng các thay đổi.


6
Để cung cấp một ví dụ trong C #: SharpLab tạo ra mã asm giống hệt nhau cho cả hai phương thức (Desktop CLR v4.7.3130.00 (clr.dll) trên x86)
VisualMelon

2
@VisualMelon funilly đủ kiểm tra tích cực: "return (((a + b)> = -1000) && ((a + b) <= 1000));" cho kết quả khác. : sharplab.io/ Kẻ
Pieter B

12
Khả năng đọc có khả năng làm cho một chương trình dễ dàng hơn để tối ưu hóa quá. Trình biên dịch có thể dễ dàng viết lại để sử dụng logic tương đương như ở trên, chỉ khi nó thực sự có thể tìm ra những gì bạn đang cố gắng làm. Nếu bạn sử dụng nhiều bithacks trường học cũ , chuyển qua lại giữa ints và con trỏ, sử dụng lại bộ lưu trữ có thể thay đổi, v.v ... trình biên dịch có thể khó hơn nhiều để chứng minh rằng một phép biến đổi là tương đương, và nó sẽ chỉ để lại những gì bạn đã viết , có thể là tối ưu.
Leushenko

1
@Corey xem chỉnh sửa.
Phil Frost

2
@Corey: câu trả lời này thực sự cho bạn biết chính xác những gì tôi đã viết trong câu trả lời của mình: không có sự khác biệt khi bạn sử dụng một trình biên dịch đàng hoàng, và thay vào đó tập trung vào khả năng đọc. Tất nhiên, nó có vẻ tốt hơn thành lập - có thể bạn tin tôi bây giờ.
Doc Brown

67

Để trả lời câu hỏi đã nêu:

Khi nào cần tối ưu hóa cho bộ nhớ so với tốc độ hiệu năng cho một phương thức?

Có hai điều bạn phải thiết lập:

  • Điều gì đang giới hạn ứng dụng của bạn?
  • Tôi có thể lấy lại phần lớn tài nguyên đó ở đâu?

Để trả lời câu hỏi đầu tiên, bạn phải biết các yêu cầu về hiệu năng cho ứng dụng của bạn là gì. Nếu không có yêu cầu về hiệu suất thì không có lý do gì để tối ưu hóa cách này hay cách khác. Các yêu cầu về hiệu suất giúp bạn đến nơi "đủ tốt".

Phương pháp bạn tự cung cấp sẽ không gây ra bất kỳ vấn đề nào về hiệu suất, nhưng có lẽ trong một vòng lặp và xử lý một lượng lớn dữ liệu, bạn phải bắt đầu nghĩ khác đi một chút về cách bạn đang tiếp cận vấn đề.

Phát hiện những gì đang giới hạn ứng dụng

Bắt đầu xem xét hành vi của ứng dụng của bạn với một màn hình hiệu suất. Theo dõi CPU, đĩa, mạng và sử dụng bộ nhớ trong khi nó đang chạy. Một hoặc nhiều mục sẽ được tối đa hóa trong khi mọi thứ khác được sử dụng vừa phải - trừ khi bạn đạt được sự cân bằng hoàn hảo, nhưng điều đó gần như không bao giờ xảy ra).

Khi bạn cần nhìn sâu hơn, thông thường bạn sẽ sử dụng một hồ sơ . Có trình biên dịch bộ nhớtrình biên dịch quy trình , và chúng đo lường những thứ khác nhau. Hành động lược tả có tác động đáng kể đến hiệu suất, nhưng bạn đang sử dụng mã của mình để tìm hiểu điều gì sai.

Giả sử bạn thấy CPU và mức sử dụng đĩa của bạn đạt đến đỉnh điểm. Trước tiên, bạn sẽ kiểm tra "điểm nóng" hoặc mã được gọi thường xuyên hơn phần còn lại hoặc mất phần trăm thời gian xử lý lâu hơn đáng kể.

Nếu bạn không thể tìm thấy bất kỳ điểm nóng nào, thì bạn sẽ bắt đầu nhìn vào bộ nhớ. Có lẽ bạn đang tạo ra nhiều đối tượng hơn mức cần thiết và bộ sưu tập rác của bạn hoạt động quá giờ.

Đòi lại hiệu suất

Nghĩ nghiêm túc. Danh sách các thay đổi sau đây theo thứ tự số tiền lãi đầu tư bạn sẽ nhận được:

  • Kiến trúc: tìm kiếm các điểm nghẹt giao tiếp
  • Thuật toán: cách bạn xử lý dữ liệu có thể cần thay đổi
  • Điểm nóng: giảm thiểu mức độ thường xuyên bạn gọi điểm nóng có thể mang lại một phần thưởng lớn
  • Tối ưu hóa vi mô: không phổ biến, nhưng đôi khi bạn thực sự cần phải nghĩ đến các điều chỉnh nhỏ (như ví dụ bạn đã cung cấp), đặc biệt nếu đó là một điểm nóng trong mã của bạn.

Trong những tình huống như thế này, bạn phải áp dụng phương pháp khoa học. Hãy đưa ra một giả thuyết, thực hiện các thay đổi và kiểm tra nó. Nếu bạn đạt được mục tiêu hiệu suất của mình, bạn đã hoàn thành. Nếu không, hãy đi đến điều tiếp theo trong danh sách.


Trả lời câu hỏi in đậm:

Khi nào thì thích hợp để sử dụng Phương pháp A so với Phương pháp B và ngược lại?

Thành thật mà nói, đây là bước cuối cùng trong việc cố gắng xử lý các vấn đề về hiệu năng hoặc bộ nhớ. Tác động của Phương pháp A so với Phương pháp B sẽ thực sự khác nhau tùy thuộc vào ngôn ngữ nền tảng (trong một số trường hợp).

Chỉ cần bất kỳ ngôn ngữ được biên dịch với trình tối ưu hóa nửa chừng sẽ tạo ra mã tương tự với một trong các cấu trúc đó. Tuy nhiên, những giả định đó không nhất thiết phải đúng trong các ngôn ngữ đồ chơi và sở hữu độc quyền không có trình tối ưu hóa.

Chính xác sẽ có tác động tốt hơn phụ thuộc vào việc sumbiến stack hay biến heap. Đây là một lựa chọn thực hiện ngôn ngữ. Ví dụ, trong C, C ++ và Java, số nguyên thủy như một intbiến là ngăn xếp theo mặc định. Mã của bạn không có tác động bộ nhớ nhiều hơn bằng cách gán cho một biến ngăn xếp so với bạn có với mã được in đầy đủ.

Các tối ưu hóa khác mà bạn có thể tìm thấy trong các thư viện C (đặc biệt là các thư viện cũ) nơi bạn có thể phải quyết định giữa việc sao chép một mảng 2 chiều xuống trước hay qua đầu tiên là tối ưu hóa phụ thuộc nền tảng. Nó đòi hỏi một số kiến ​​thức về cách chipset bạn đang nhắm mục tiêu tối ưu hóa truy cập bộ nhớ tốt nhất. Có sự khác biệt tinh tế giữa các kiến ​​trúc.

Điểm mấu chốt là tối ưu hóa là sự kết hợp giữa nghệ thuật và khoa học. Nó đòi hỏi một số suy nghĩ phê phán, cũng như mức độ linh hoạt trong cách bạn tiếp cận vấn đề. Hãy tìm những điều lớn lao trước khi bạn đổ lỗi cho những điều nhỏ nhặt.


2
Câu trả lời này tập trung vào câu hỏi của tôi nhiều nhất và không bị cuốn vào các ví dụ mã hóa của tôi, tức là Phương pháp A và Phương pháp B.
Corey P

18
Tôi cảm thấy như đây là câu trả lời chung cho "Làm thế nào để bạn giải quyết các tắc nghẽn hiệu suất" nhưng bạn sẽ khó có thể xác định việc sử dụng bộ nhớ tương đối từ một chức năng cụ thể dựa trên việc nó có 4 hoặc 5 biến bằng phương pháp này hay không. Tôi cũng đặt câu hỏi mức độ tối ưu hóa này có liên quan như thế nào khi trình biên dịch (hoặc trình thông dịch) có thể hoặc không thể tối ưu hóa mức này.
Eric

@Eric, như tôi đã đề cập, danh mục cải tiến hiệu suất cuối cùng sẽ là tối ưu hóa vi mô của bạn. Cách duy nhất để có một dự đoán tốt nếu nó sẽ có bất kỳ tác động nào là bằng cách đo hiệu suất / bộ nhớ trong một hồ sơ. Rất hiếm khi các loại cải tiến đó có kết quả, nhưng trong các vấn đề hiệu suất nhạy cảm về thời gian, bạn có trong các trình giả lập, một vài thay đổi được đặt tốt như thế có thể là sự khác biệt giữa việc đạt được mục tiêu thời gian của bạn hay không. Tôi nghĩ rằng tôi có thể đếm bằng một tay số lần đã trả trong hơn 20 năm làm việc trên phần mềm, nhưng nó không phải là con số không.
Berin Loritsch

@BerinLoritsch Một lần nữa, nói chung tôi đồng ý với bạn, nhưng trong trường hợp cụ thể này thì tôi không. Tôi đã cung cấp câu trả lời của riêng mình, nhưng cá nhân tôi chưa thấy bất kỳ công cụ nào sẽ gắn cờ hoặc thậm chí cung cấp cho bạn các cách để xác định các vấn đề hiệu suất liên quan đến kích thước bộ nhớ của chức năng.
Eric

@DocBrown, tôi đã khắc phục điều đó. Về câu hỏi thứ hai, tôi khá đồng ý với bạn.
Berin Loritsch

45

"Điều này sẽ làm giảm bộ nhớ" - em, không. Ngay cả khi điều này là đúng (mà, đối với bất kỳ trình biên dịch tử tế nào là không), sự khác biệt có lẽ sẽ không đáng kể đối với bất kỳ tình huống thực tế nào.

Tuy nhiên, tôi khuyên bạn nên sử dụng phương pháp A * (phương pháp A với một chút thay đổi):

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)
}

nhưng vì hai lý do hoàn toàn khác nhau:

  • bằng cách đặt tên biến sgiải thích, mã trở nên rõ ràng hơn

  • nó tránh để có cùng logic logic hai lần trong mã, do đó mã trở nên DRY hơn, điều đó có nghĩa là ít lỗi hơn khi thay đổi.


36
Tôi sẽ làm sạch nó hơn nữa và đi với "return sum> -1000 && sum <1000;".
17 của 26

36
@Corey bất kỳ trình tối ưu hóa tốt nào cũng sẽ sử dụng thanh ghi CPU cho sumbiến, do đó dẫn đến việc sử dụng bộ nhớ bằng không. Và ngay cả khi không, đây chỉ là một từ duy nhất của bộ nhớ trong một phương thức Lá Lá. Xem xét cách Java hoặc C # gây lãng phí bộ nhớ đến mức nào có thể là do mô hình đối tượng và GC của chúng, một intbiến cục bộ theo nghĩa đen không sử dụng bất kỳ bộ nhớ đáng chú ý nào. Đây là tối ưu hóa vô nghĩa.
amon

10
@Corey: nếu nó " phức tạp hơn một chút ", có lẽ nó sẽ không trở thành "một cách sử dụng bộ nhớ đáng chú ý". Có thể nếu bạn xây dựng một ví dụ thực sự phức tạp hơn, nhưng điều đó làm cho nó trở thành một câu hỏi khác. Cũng lưu ý, chỉ vì bạn không tạo một biến cụ thể cho một biểu thức, đối với các kết quả trung gian phức tạp, môi trường thời gian chạy vẫn có thể tạo bên trong các đối tượng tạm thời, do đó, nó hoàn toàn phụ thuộc vào chi tiết về ngôn ngữ, môi trường, mức độ tối ưu hóa và bất cứ điều gì bạn gọi là "đáng chú ý".
Doc Brown

8
Ngoài các điểm trên, tôi khá chắc chắn cách C # / Java chọn lưu trữ sumsẽ là một chi tiết triển khai và tôi nghi ngờ bất kỳ ai cũng có thể đưa ra một trường hợp thuyết phục về việc liệu một thủ thuật ngớ ngẩn như tránh một địa phương intsẽ dẫn đến điều này hay lượng sử dụng bộ nhớ đó trong dài hạn. Khả năng đọc IMO là quan trọng hơn. Khả năng đọc có thể chủ quan, nhưng FWIW, cá nhân tôi muốn bạn không bao giờ thực hiện cùng một tính toán hai lần, không phải vì sử dụng CPU, mà vì tôi chỉ phải kiểm tra bổ sung của bạn một lần khi tôi tìm lỗi.
jrh

2
... cũng lưu ý rằng các ngôn ngữ được thu gom rác nói chung là không thể đoán trước được, "biển nhớ bộ nhớ" mà (dù sao đối với C #) chỉ có thể được dọn sạch khi cần , tôi nhớ việc tạo một chương trình phân bổ RAM gigabyte và nó chỉ bắt đầu " dọn dẹp "sau khi bộ nhớ trở nên khan hiếm. Nếu GC không cần chạy, nó có thể mất thời gian ngọt ngào và tiết kiệm CPU của bạn cho các vấn đề cấp bách hơn.
jrh

35

Bạn có thể làm tốt hơn cả những người có

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

Hầu hết các bộ xử lý (và do đó trình biên dịch) có thể thực hiện abs () trong một hoạt động. Bạn không chỉ có số tiền ít hơn, mà còn có ít so sánh hơn, thường đắt hơn về mặt tính toán. Nó cũng loại bỏ sự phân nhánh, điều tồi tệ hơn nhiều trên hầu hết các bộ xử lý vì nó dừng khả năng đường ống là có thể.

Người phỏng vấn, như những câu trả lời khác đã nói, là đời sống thực vật và không có doanh nghiệp nào thực hiện một cuộc phỏng vấn kỹ thuật.

Điều đó nói rằng, câu hỏi của ông là hợp lệ. Và câu trả lời khi bạn tối ưu hóa và làm thế nào, là khi bạn chứng minh điều đó là cần thiết và bạn đã mô tả nó để chứng minh chính xác những phần nào cần nó . Knuth nổi tiếng nói rằng tối ưu hóa sớm là gốc rễ của mọi tội lỗi, bởi vì quá dễ dàng để thử các phần không quan trọng bằng vàng, hoặc thực hiện các thay đổi (như người phỏng vấn của bạn) mà không có tác dụng, trong khi bỏ lỡ những nơi thực sự cần nó. Cho đến khi bạn có bằng chứng cứng thực sự cần thiết, sự rõ ràng của mã là mục tiêu quan trọng hơn.

Chỉnh sửa FabioTurati chỉ ra một cách chính xác rằng đây là ý nghĩa logic trái ngược với nguyên bản, (lỗi của tôi!), Và điều này minh họa một tác động nữa từ trích dẫn của Knuth khi chúng ta có nguy cơ phá vỡ mã trong khi chúng ta đang cố gắng tối ưu hóa nó.


2
@Corey, tôi khá chắc chắn rằng Graham ghim yêu cầu "anh ấy đã thách thức tôi giải quyết vấn đề tương tự với ít biến số hơn" như mong đợi. Nếu tôi là người phỏng vấn, tôi mong đợi câu trả lời đó, không chuyển a+bsang ifvà thực hiện hai lần. Bạn hiểu sai "Anh ấy hài lòng và nói rằng điều này sẽ làm giảm việc sử dụng bộ nhớ bằng phương pháp này" - anh ấy rất tốt với bạn, che giấu sự thất vọng của mình với lời giải thích vô nghĩa này về bộ nhớ. Bạn không nên nghiêm túc đặt câu hỏi ở đây. Bạn đã tìm được việc chưa? Tôi đoán bạn đã không :-(
Sinatr

1
Bạn đang áp dụng 2 biến đổi cùng một lúc: bạn đã biến 2 điều kiện thành 1, sử dụng abs()và bạn cũng có một biến đổi return, thay vì có một biến đổi khi điều kiện là đúng ("nếu nhánh") và một điều kiện khác khi nó sai ( "Chi nhánh khác"). Khi bạn thay đổi mã như thế này, hãy cẩn thận: có nguy cơ vô tình viết một hàm trả về true khi nó trả về false và ngược lại. Đó chính xác là những gì đã xảy ra ở đây. Tôi biết bạn đang tập trung vào một thứ khác, và bạn đã hoàn thành tốt công việc đó. Tuy nhiên, điều này có thể dễ dàng khiến bạn mất việc ...
Fabio Turati

2
@FabioTurati Phát hiện tốt - cảm ơn! Tôi sẽ cập nhật câu trả lời. Và đó là một điểm tốt về tái cấu trúc và tối ưu hóa, điều này làm cho trích dẫn của Knuth thậm chí còn phù hợp hơn. Chúng ta nên chứng minh rằng chúng ta cần tối ưu hóa trước khi chấp nhận rủi ro.
Graham

2
Hầu hết các bộ xử lý (và do đó trình biên dịch) có thể thực hiện abs () trong một hoạt động. Thật không may, không phải là trường hợp cho số nguyên. ARM64 có phủ định có điều kiện, nó có thể sử dụng nếu các cờ đã được đặt từ một addsvà ARM đã cung cấp đảo ngược phụ ( rsblt= Reverse-sub nếu ít-tha) nhưng mọi thứ khác đều yêu cầu nhiều hướng dẫn bổ sung để thực hiện abs(a+b)hoặc abs(a). godbolt.org/z/Ok_Con hiển thị đầu ra asm x86, ARM, AArch64, PowerPC, MIPS và RISC-V. Chỉ bằng cách chuyển đổi so sánh thành một phạm vi - kiểm tra xem (unsigned)(a+b+999) <= 1998Ugcc có thể tối ưu hóa nó như trong câu trả lời của Phil không.
Peter Cordes

2
Mã "được cải thiện" trong câu trả lời này vẫn còn sai, vì nó tạo ra một câu trả lời khác cho IsSumInRange(INT_MIN, 0). Mã ban đầu trả về falseINT_MIN+0 > 1000 || INT_MIN+0 < -1000; nhưng mã "mới và cải tiến" trả về trueabs(INT_MIN+0) < 1000. (Hoặc, trong một số ngôn ngữ, nó sẽ đưa ra một ngoại lệ hoặc có hành vi không xác định. Kiểm tra danh sách địa phương của bạn.)
Quuxplusone

16

Khi nào thì thích hợp để sử dụng Phương pháp A so với Phương pháp B và ngược lại?

Phần cứng là giá rẻ; lập trình rất tốn kém . Vì vậy, chi phí thời gian hai bạn lãng phí cho câu hỏi này có lẽ tồi tệ hơn nhiều so với câu trả lời.

Bất kể, hầu hết các trình biên dịch hiện đại sẽ tìm cách tối ưu hóa biến cục bộ thành một thanh ghi (thay vì phân bổ không gian ngăn xếp), vì vậy các phương thức có thể giống hệt nhau về mã thực thi. Vì lý do này, hầu hết các nhà phát triển sẽ chọn tùy chọn truyền đạt ý định rõ ràng nhất (xem Viết mã thực sự rõ ràng (ROC) ). Theo tôi, đó sẽ là Phương pháp A.

Mặt khác, nếu đây hoàn toàn là một bài tập học thuật, bạn có thể có cả hai thế giới tốt nhất với Phương pháp C:

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

17
a+=blà một mẹo gọn gàng nhưng tôi phải đề cập (chỉ trong trường hợp nó không được ngụ ý từ phần còn lại của câu trả lời), từ các phương pháp kinh nghiệm của tôi mà việc lộn xộn với các tham số có thể rất khó để gỡ lỗi và duy trì.
jrh

1
Tôi đồng ý @jrh. Tôi là một người ủng hộ mạnh mẽ cho ROC, và những thứ đó là bất cứ điều gì nhưng.
John Wu

3
"Phần cứng là rẻ, lập trình viên là đắt tiền." Trong thế giới điện tử tiêu dùng, tuyên bố đó là sai. Nếu bạn bán hàng triệu đơn vị, thì đó là một khoản đầu tư rất tốt để chi 500.000 đô la chi phí phát triển bổ sung để tiết kiệm 0,10 đô la cho chi phí phần cứng cho mỗi đơn vị.
Bart van Ingen Schenau

2
@JohnWu: Bạn đã đơn giản hóa việc ifkiểm tra, nhưng quên đảo ngược kết quả so sánh; chức năng của bạn bây giờ đang trở lại truekhi a + bkhông trong phạm vi. Hoặc thêm a !vào bên ngoài điều kiện ( return !(a > 1000 || a < -1000)) hoặc phân phối !, đảo ngược các thử nghiệm, để có được return a <= 1000 && a >= -1000;hoặc để kiểm tra phạm vi độc đáo,return -1000 <= a && a <= 1000;
ShadowRanger

1
@JohnWu: Vẫn còn hơi off tại các trường hợp cạnh, logic phân phối đòi hỏi <=/ >=, không </ >(với </ >, 1000 và -1000 được coi là đồng ra khỏi phạm vi, mã ban đầu đối xử với họ như trong phạm vi).
ShadowRanger

11

Tôi sẽ tối ưu hóa cho dễ đọc. Phương pháp 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);
}

Phương pháp nhỏ chỉ làm 1 việc nhưng rất dễ lý luận.

(Đây là sở thích cá nhân, tôi thích thử nghiệm tích cực thay vì âm tính, mã ban đầu của bạn thực sự đang thử nghiệm xem giá trị KHÔNG nằm ngoài phạm vi hay không.)


5
Điều này. (Nhận xét nâng cao ở trên tương tự re: khả năng đọc). Cách đây 30 năm, khi chúng tôi làm việc với các máy có RAM dưới 1mb, việc nén hiệu năng là cần thiết - giống như vấn đề y2k, nhận được vài trăm nghìn bản ghi mà mỗi bộ nhớ bị lãng phí vài byte do các vars không được sử dụng và tài liệu tham khảo, vv và nó tăng lên nhanh chóng khi bạn chỉ có 256k RAM. Bây giờ chúng tôi đang xử lý các máy có nhiều gigabyte RAM, tiết kiệm thậm chí một vài MB sử dụng RAM so với khả năng đọc và khả năng duy trì mã không phải là một giao dịch tốt.
ivanivan

@ivanivan: Tôi không nghĩ "vấn đề y2k" thực sự là về bộ nhớ. Từ quan điểm nhập dữ liệu, nhập hai chữ số hiệu quả hơn nhập bốn và giữ mọi thứ khi nhập dễ dàng hơn chuyển đổi chúng sang một số dạng khác.
supercat

10
Bây giờ bạn phải theo dõi qua 2 chức năng để xem những gì đang xảy ra. Bạn không thể lấy nó theo mệnh giá, bởi vì bạn không thể nói từ tên cho dù đây là giới hạn bao gồm hay độc quyền. Và nếu bạn thêm thông tin đó, tên của hàm dài hơn mã để thể hiện nó.
Peter

1
Tối ưu hóa khả năng đọc và thực hiện các chức năng nhỏ, dễ lý do - chắc chắn, đồng ý. Nhưng tôi phản đối kịch liệt rằng đổi tên abđể number1number2hỗ trợ khả năng đọc dưới mọi hình thức. Ngoài ra, việc đặt tên các hàm của bạn không nhất quán: tại sao IsSumInRangemã cứng phạm vi nếu IsValueInRangechấp nhận nó làm đối số?
leftaroundabout

Hàm số 1 có thể tràn. (Giống như mã của các câu trả lời khác.) Mặc dù độ phức tạp của mã an toàn tràn là một đối số để đưa nó vào một hàm.
philipxy

6

Nói tóm lại, tôi không nghĩ câu hỏi có liên quan nhiều đến điện toán hiện tại, nhưng từ góc độ lịch sử, đó là một bài tập suy nghĩ thú vị.

Người phỏng vấn của bạn có khả năng là một fan hâm mộ của Tháng người đàn ông huyền thoại. Trong cuốn sách, Fred Brooks đưa ra trường hợp các lập trình viên thường sẽ cần hai phiên bản chức năng chính trong hộp công cụ của họ: phiên bản tối ưu hóa bộ nhớ và phiên bản tối ưu hóa cpu. Fred dựa trên kinh nghiệm của mình dẫn đầu sự phát triển của hệ điều hành IBM System / 360, nơi các máy có thể có ít nhất 8 kilobyte RAM. Trong các máy như vậy, bộ nhớ cần thiết cho các biến cục bộ trong các hàm có thể có khả năng quan trọng, đặc biệt là nếu trình biên dịch không tối ưu hóa chúng một cách hiệu quả (hoặc nếu mã được viết bằng ngôn ngữ lắp ráp trực tiếp).

Trong thời đại hiện nay, tôi nghĩ bạn sẽ khó tìm được một hệ thống trong đó sự hiện diện hay vắng mặt của một biến cục bộ trong một phương thức sẽ tạo ra sự khác biệt đáng chú ý. Đối với một biến thành vấn đề, phương pháp sẽ cần phải được đệ quy với đệ quy sâu dự kiến. Thậm chí sau đó, có khả năng độ sâu ngăn xếp sẽ bị vượt quá gây ra ngoại lệ Stack Overflow trước khi chính biến đó gây ra sự cố. Kịch bản thực tế duy nhất có thể là một vấn đề là với các mảng rất lớn, được phân bổ trên ngăn xếp theo phương pháp đệ quy. Nhưng điều đó cũng khó xảy ra vì tôi nghĩ rằng hầu hết các nhà phát triển sẽ nghĩ hai lần về các bản sao không cần thiết của các mảng lớn.


4

Sau khi gán s = a + b; các biến a và b không được sử dụng nữa. Do đó, không có bộ nhớ nào được sử dụng cho s nếu bạn không sử dụng trình biên dịch hoàn toàn bị hỏng não; bộ nhớ đã được sử dụng cho a và b được sử dụng lại.

Nhưng tối ưu hóa chức năng này là hoàn toàn vô nghĩa. Nếu bạn có thể tiết kiệm dung lượng, nó có thể là 8 byte trong khi hàm đang chạy (được phục hồi khi hàm trả về), vì vậy hoàn toàn vô nghĩa. Nếu bạn có thể tiết kiệm thời gian, nó sẽ là một số nano giây. Tối ưu hóa điều này là một sự lãng phí hoàn toàn thời gian.


3

Các biến loại giá trị cục bộ được phân bổ trên ngăn xếp hoặc (nhiều khả năng cho các đoạn mã nhỏ như vậy) sử dụng các thanh ghi trong bộ xử lý và không bao giờ thấy bất kỳ RAM nào. Dù bằng cách nào họ cũng sống ngắn và không có gì phải lo lắng. Bạn bắt đầu xem xét việc sử dụng bộ nhớ khi bạn cần đệm hoặc xếp hàng các thành phần dữ liệu trong các bộ sưu tập có khả năng lớn và tồn tại lâu dài.

Sau đó, nó phụ thuộc vào những gì bạn quan tâm nhất cho ứng dụng của bạn. Tốc độ xử lý? Thời gian đáp ứng? Mức chiếm dụng bộ nhớ? Bảo trì? Sự nhất quán trong thiết kế? Tùy vào bạn.


4
Nitpicking: Ít nhất .NET (ngôn ngữ của bài đăng không được chỉ định) không đảm bảo bất kỳ đảm bảo nào về các biến cục bộ được phân bổ "trên ngăn xếp". Xem "ngăn xếp là một chi tiết triển khai" của Eric Lippert.
jrh

1
@jrh Các biến cục bộ trên stack hoặc heap có thể là một chi tiết triển khai, nhưng nếu ai đó thực sự muốn một biến trên stack đó stackallocvà ngay bây giờ Span<T>. Có thể hữu ích trong một điểm nóng, sau khi hồ sơ. Ngoài ra, một số tài liệu xung quanh cấu trúc ngụ ý rằng các loại giá trị thể nằm trên ngăn xếp trong khi các loại tham chiếu sẽ không có. Dù sao, tốt nhất bạn có thể tránh một chút GC.
Bob

2

Như những câu trả lời khác đã nói, bạn cần suy nghĩ những gì bạn đang tối ưu hóa.

Trong ví dụ này, tôi nghi ngờ rằng bất kỳ trình biên dịch tử tế nào cũng sẽ tạo mã tương đương cho cả hai phương thức, vì vậy quyết định sẽ không ảnh hưởng đến thời gian chạy hoặc bộ nhớ!

Những gì nó không ảnh hưởng đến là khả năng đọc của mã này. (Mã dành cho con người đọc, không chỉ máy tính.) Không có quá nhiều sự khác biệt giữa hai ví dụ; khi tất cả những thứ khác đều bằng nhau, tôi coi sự ngắn gọn là một đức tính, vì vậy tôi có thể chọn Phương pháp B. Nhưng tất cả những thứ khác hiếm khi bằng nhau, và trong trường hợp thực tế phức tạp hơn, nó có thể có ảnh hưởng lớn.

Những điều cần cân nhắc:

  • Liệu các biểu hiện trung gian có bất kỳ tác dụng phụ? Nếu nó gọi bất kỳ hàm không tinh khiết hoặc cập nhật bất kỳ biến nào, thì tất nhiên việc sao chép nó sẽ là vấn đề chính xác, không chỉ là kiểu dáng.
  • Làm thế nào phức tạp là biểu thức trung gian? Nếu nó thực hiện nhiều tính toán và / hoặc gọi các hàm, thì trình biên dịch có thể không thể tối ưu hóa nó, và vì vậy điều này sẽ ảnh hưởng đến hiệu suất. (Mặc dù, như Knuth đã nói , chúng ta nên quên đi những hiệu quả nhỏ, nói về 97% thời gian của nhóm.)
  • Liệu các biến trung gian có bất kỳ ý nghĩa ? Nó có thể được đặt một cái tên giúp giải thích những gì đang xảy ra? Một tên ngắn nhưng nhiều thông tin có thể giải thích mã tốt hơn, trong khi một tên vô nghĩa chỉ là nhiễu hình ảnh.
  • Bao lâu là biểu thức trung gian? Nếu dài, sau đó sao chép nó có thể làm cho mã dài hơn và khó đọc hơn (đặc biệt là nếu nó buộc ngắt dòng); nếu không, sự trùng lặp có thể ngắn hơn tất cả.

1

Như nhiều câu trả lời đã chỉ ra, cố gắng điều chỉnh chức năng này bằng các trình biên dịch hiện đại sẽ không tạo ra bất kỳ sự khác biệt nào. Một trình tối ưu hóa rất có thể tìm ra giải pháp tốt nhất (bỏ phiếu cho câu trả lời cho thấy mã trình biên dịch để chứng minh điều đó!). Bạn nói rằng mã trong cuộc phỏng vấn không chính xác là mã bạn được yêu cầu so sánh, vì vậy có lẽ ví dụ thực tế có ý nghĩa hơn một chút.

Nhưng hãy xem xét lại câu hỏi này: đây là một câu hỏi phỏng vấn. Vì vậy, vấn đề thực sự là, bạn nên trả lời như thế nào khi cho rằng bạn muốn thử và nhận công việc?

Chúng ta cũng giả sử rằng người phỏng vấn biết những gì họ đang nói và họ chỉ đang cố gắng để xem những gì bạn biết.

Tôi sẽ đề cập rằng, bỏ qua trình tối ưu hóa, lần đầu tiên có thể tạo một biến tạm thời trên ngăn xếp trong khi lần thứ hai thì không, nhưng sẽ thực hiện phép tính hai lần. Do đó, lần đầu tiên sử dụng nhiều bộ nhớ hơn nhưng nhanh hơn.

Bạn có thể đề cập rằng dù sao đi nữa, một phép tính có thể yêu cầu một biến tạm thời để lưu trữ kết quả (để nó được so sánh), do đó, việc bạn đặt tên biến đó hay không có thể không tạo ra bất kỳ sự khác biệt nào.

Sau đó tôi sẽ đề cập rằng trong thực tế, mã sẽ được tối ưu hóa và rất có thể mã máy tương đương sẽ được tạo vì tất cả các biến là cục bộ. Tuy nhiên, nó phụ thuộc vào trình biên dịch bạn đang sử dụng (cách đây không lâu tôi có thể cải thiện hiệu suất hữu ích bằng cách khai báo một biến cục bộ là "cuối cùng" trong Java).

Bạn có thể đề cập rằng ngăn xếp trong mọi trường hợp nằm trong trang bộ nhớ của chính nó, vì vậy trừ khi biến phụ của bạn khiến ngăn xếp tràn ra trang, thực tế nó sẽ không phân bổ thêm bộ nhớ. Nếu nó tràn, nó sẽ muốn một trang hoàn toàn mới.

Tôi sẽ đề cập rằng một ví dụ thực tế hơn có thể là lựa chọn có nên sử dụng bộ đệm để giữ kết quả của nhiều tính toán hay không và điều này sẽ đặt ra một câu hỏi về cpu so với bộ nhớ.

Tất cả điều này chứng tỏ rằng bạn biết những gì bạn đang nói về.

Tôi sẽ để nó đến cuối cùng để nói rằng tốt hơn là tập trung vào sự sẵn sàng thay thế. Mặc dù đúng trong trường hợp này, trong ngữ cảnh phỏng vấn, nó có thể được hiểu là "Tôi không biết về hiệu suất nhưng mã của tôi đọc giống như một câu chuyện của Janet và John ".

Những gì bạn không nên làm là tìm ra những tuyên bố nhạt nhẽo thông thường về cách tối ưu hóa mã là không cần thiết, đừng tối ưu hóa cho đến khi bạn đã mã hóa mã (điều này chỉ cho thấy bạn không thể thấy mã xấu cho mình), chi phí phần cứng ít hơn so với lập trình viên và xin vui lòng, xin vui lòng, đừng trích dẫn Knuth "sớm quá blah blah ...".

Hiệu suất mã là một vấn đề thực sự trong rất nhiều tổ chức và nhiều tổ chức cần các lập trình viên hiểu nó.

Đặc biệt, với các tổ chức như Amazon, một số mã có đòn bẩy rất lớn. Một đoạn mã có thể được triển khai trên hàng ngàn máy chủ hoặc hàng triệu thiết bị và có thể được gọi là hàng tỷ lần mỗi ngày mỗi ngày trong năm. Có thể có hàng ngàn đoạn tương tự. Sự khác biệt giữa một thuật toán xấu và một thuật toán tốt có thể dễ dàng là một yếu tố của một ngàn. Thực hiện các con số và nhiều thứ này lên: nó tạo ra sự khác biệt. Chi phí tiềm năng cho việc tổ chức mã không thực hiện có thể rất đáng kể hoặc thậm chí gây tử vong nếu một hệ thống hết dung lượng.

Furthmore, nhiều tổ chức trong số này làm việc trong một môi trường cạnh tranh. Vì vậy, bạn không thể chỉ nói với khách hàng của mình mua một máy tính lớn hơn nếu phần mềm của đối thủ cạnh tranh đã hoạt động tốt trên phần cứng mà họ có hoặc nếu phần mềm chạy trên điện thoại di động và không thể nâng cấp. Một số ứng dụng đặc biệt quan trọng về hiệu năng (trò chơi và ứng dụng di động xuất hiện) và có thể sống hoặc chết tùy theo mức độ đáp ứng hoặc tốc độ của chúng.

Cá nhân tôi đã làm việc hơn hai thập kỷ cho nhiều dự án mà các hệ thống bị lỗi hoặc không sử dụng được do vấn đề hiệu năng và tôi đã được gọi để tối ưu hóa các hệ thống đó và trong mọi trường hợp là do mã xấu được viết bởi những lập trình viên không hiểu tác động của những gì họ đã viết. Furthmore, nó không bao giờ là một đoạn mã, nó luôn ở khắp mọi nơi. Khi tôi bật lên, đó là cách muộn để bắt đầu suy nghĩ về hiệu suất: thiệt hại đã được thực hiện.

Hiểu hiệu suất mã là một kỹ năng tốt để có cùng cách với hiểu chính xác mã và kiểu mã. Nó đi ra khỏi thực tế. Lỗi hiệu suất có thể tệ như lỗi chức năng. Nếu hệ thống không hoạt động, nó không hoạt động. Không quan trọng tại sao. Tương tự, hiệu suất và tính năng không bao giờ được sử dụng đều xấu.

Vì vậy, nếu người phỏng vấn hỏi bạn về hiệu suất, tôi sẽ khuyên bạn nên thử và thể hiện càng nhiều kiến ​​thức càng tốt. Nếu câu hỏi có vẻ xấu, hãy lịch sự chỉ ra lý do tại sao bạn nghĩ rằng đó không phải là vấn đề trong trường hợp đó. Đừng trích dẫn Knuth.


0

Trước tiên bạn nên tối ưu hóa cho chính xác.

Hàm của bạn không thành công cho các giá trị đầu vào gần với Int.MaxValue:

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

Điều này trả về đúng vì tổng số tràn đến -400. Hàm này cũng không hoạt động cho a = int.MinValue + 200. (thêm không chính xác đến "400")

Chúng tôi sẽ không biết những gì người phỏng vấn đang tìm kiếm trừ khi anh ta hoặc cô chimes trong, nhưng "tràn là có thật" .

Trong một tình huống phỏng vấn, hãy đặt câu hỏi để làm rõ phạm vi của vấn đề: các giá trị đầu vào tối đa và tối thiểu được phép là gì? Khi bạn có những thứ đó, bạn có thể ném ngoại lệ nếu người gọi gửi các giá trị ngoài phạm vi. Hoặc (trong C #), bạn có thể sử dụng phần {} đã kiểm tra, phần này sẽ đưa ra một ngoại lệ khi tràn. Vâng, nó làm việc nhiều hơn và phức tạp hơn, nhưng đôi khi đó là những gì nó cần.


Các phương pháp chỉ là ví dụ. Chúng không được viết là chính xác, nhưng để minh họa cho câu hỏi thực tế. Cảm ơn cho đầu vào mặc dù!
Corey P

Tôi nghĩ rằng câu hỏi phỏng vấn hướng vào hiệu suất, vì vậy bạn cần trả lời ý định của câu hỏi. Người phỏng vấn không hỏi về hành vi ở giới hạn. Nhưng dù sao cũng là điểm thú vị.
rghome

1
@Corey Những người phỏng vấn giỏi như câu hỏi cho 1) đánh giá khả năng của ứng viên liên quan đến vấn đề, như được đề xuất bởi rghome ở đây cũng 2) như là một sự mở ra cho các vấn đề lớn hơn (như tính chính xác của chức năng không được nói) và độ sâu của kiến ​​thức liên quan - điều này còn hơn thế trong các cuộc phỏng vấn nghề nghiệp sau này - chúc may mắn.
chux

0

Câu hỏi của bạn nên là: "Tôi có cần tối ưu hóa điều này không?".

Phiên bản A và B khác nhau ở một chi tiết quan trọng giúp A thích hợp hơn, nhưng nó không liên quan đến tối ưu hóa: Bạn không lặp lại mã.

"Tối ưu hóa" thực tế được gọi là loại bỏ phổ biến phụ, đó là điều mà hầu hết mọi trình biên dịch đều làm. Một số thực hiện tối ưu hóa cơ bản này ngay cả khi tắt tối ưu hóa. Vì vậy, đó không thực sự là một tối ưu hóa (mã được tạo gần như chắc chắn sẽ giống hệt nhau trong mọi trường hợp).

Nhưng nếu nó không phải là một tối ưu hóa, thì tại sao nó lại thích hợp hơn? Được rồi, bạn đừng lặp lại mã, ai quan tâm!

Vâng, trước hết, bạn không có nguy cơ vô tình nhận được một nửa mệnh đề điều kiện sai. Nhưng quan trọng hơn, ai đó đọc mã này có thể tìm hiểu ngay những gì bạn đang cố gắng làm, thay vì một if((((wtf||is||this||longexpression))))trải nghiệm. Những gì người đọc nhận thấy là if(one || theother), đó là một điều tốt. Không hiếm khi, tôi tình cờ rằng bạn là người khác đọc mã của chính bạn ba năm sau đó và nghĩ rằng "WTF có nghĩa là gì?". Trong trường hợp đó, nó luôn hữu ích nếu mã của bạn ngay lập tức truyền đạt ý định đó là gì. Với một biểu hiện con chung được đặt tên đúng, đó là trường hợp.
Ngoài ra, nếu bất cứ lúc nào trong tương lai, bạn quyết định rằng ví dụ: bạn cần thay đổi a+bthành a-b, bạn phải thay đổi mộtvị trí, không phải hai. Và không có nguy cơ (một lần nữa) gặp sai lầm thứ hai do tai nạn.

Về câu hỏi thực tế của bạn, những gì bạn nên tối ưu hóa, trước hết mã của bạn phải chính xác . Đây là điều hoàn toàn quan trọng nhất. Mã không đúng là mã xấu, thậm chí ngay cả khi không chính xác, nó "hoạt động tốt" hoặc ít nhất có vẻ như nó hoạt động tốt. Sau đó, mã phải được đọc (có thể đọc được bởi một người không quen thuộc với nó).
Để tối ưu hóa ... chắc chắn người ta không nên cố tình viết mã chống tối ưu hóa và chắc chắn tôi không nói rằng bạn không nên dành một suy nghĩ cho thiết kế trước khi bắt đầu (chẳng hạn như chọn thuật toán phù hợp cho vấn đề, không phải là ít hiệu quả nhất).

Nhưng đối với hầu hết các ứng dụng, hầu hết thời gian, hiệu suất mà bạn có được sau khi chạy mã chính xác, có thể đọc được bằng thuật toán hợp lý thông qua trình biên dịch tối ưu hóa là tốt, không cần phải lo lắng.

Nếu đó không phải là trường hợp, tức là nếu hiệu suất của ứng dụng thực sự không đáp ứng các yêu cầu và chỉ sau đó , bạn nên lo lắng về việc thực hiện tối ưu hóa cục bộ như cách bạn đã thử. Mặc dù vậy, tốt hơn là bạn sẽ xem xét lại thuật toán cấp cao nhất. Nếu bạn gọi một hàm 500 lần thay vì 50.000 lần vì thuật toán tốt hơn, điều này có tác động lớn hơn so với việc tiết kiệm ba chu kỳ xung nhịp trên tối ưu hóa vi mô. Nếu bạn không trì hoãn hàng trăm chu kỳ trên một bộ nhớ ngẫu nhiên truy cập mọi lúc, điều này có tác động lớn hơn so với việc thực hiện thêm một vài phép tính rẻ tiền, v.v.

Tối ưu hóa là một vấn đề khó khăn (bạn có thể viết toàn bộ sách về điều đó và không có kết thúc), và dành thời gian để tối ưu hóa một cách mù quáng một số điểm cụ thể (mà thậm chí không biết liệu đó có phải là nút cổ chai hay không!) Thường lãng phí thời gian. Không có hồ sơ, tối ưu hóa là rất khó để có được đúng.

Nhưng theo nguyên tắc thông thường, khi bạn bị mù và chỉ cần / muốn làm gì đó , hoặc như một chiến lược mặc định chung, tôi sẽ đề nghị tối ưu hóa cho "bộ nhớ".
Tối ưu hóa cho "bộ nhớ" (đặc biệt là địa phương không gian và kiểu truy cập) thường mang lại lợi ích vì không giống như trước đây khi mọi thứ đều "giống nhau", ngày nay, việc truy cập RAM là một trong những thứ đắt nhất (thiếu đọc từ đĩa!) về nguyên tắc bạn có thể làm. Trong khi đó, ALU, mặt khác, lại rẻ và nhanh hơn mỗi tuần. Băng thông bộ nhớ và độ trễ không cải thiện gần như nhanh. Địa phương tốt và các mẫu truy cập tốt có thể dễ dàng tạo ra sự khác biệt 5x (20x trong các ví dụ cực đoan, khó chịu) trong thời gian chạy so với các mẫu truy cập xấu trong các ứng dụng nặng dữ liệu. Hãy tốt với bộ nhớ cache của bạn, và bạn sẽ là một người hạnh phúc.

Để đặt đoạn trước vào quan điểm, hãy xem xét những gì khác nhau mà bạn có thể làm chi phí bạn. Thực hiện một cái gì đó như a+bmất (nếu không được tối ưu hóa) một hoặc hai chu kỳ, nhưng CPU thường có thể bắt đầu một số hướng dẫn trong mỗi chu kỳ và có thể truyền các hướng dẫn không phụ thuộc nên thực tế hơn, nó chỉ khiến bạn mất khoảng nửa chu kỳ hoặc ít hơn. Lý tưởng nhất, nếu trình biên dịch tốt trong việc lập lịch trình, và tùy thuộc vào tình huống, nó có thể có giá bằng không.
Tìm nạp dữ liệu ("bộ nhớ") sẽ khiến bạn mất 4-5 chu kỳ nếu bạn may mắn và trong L1 và khoảng 15 chu kỳ nếu bạn không may mắn như vậy (cú đánh L2). Nếu dữ liệu hoàn toàn không có trong bộ đệm, phải mất vài trăm chu kỳ. Nếu mẫu truy cập hỗn loạn của bạn vượt quá khả năng của TLB (dễ thực hiện chỉ với ~ 50 mục), hãy thêm vài trăm chu kỳ. Nếu mẫu truy cập hỗn loạn của bạn thực sự gây ra lỗi trang, nó sẽ khiến bạn mất vài chục nghìn chu kỳ trong trường hợp tốt nhất và vài triệu trong trường hợp xấu nhất.
Bây giờ hãy nghĩ về nó, những gì bạn muốn tránh khẩn cấp nhất?


0

Khi nào cần tối ưu hóa cho bộ nhớ so với tốc độ hiệu năng cho một phương thức?

Sau khi có được chức năng ngay trước . Sau đó chọn lọc quan tâm đến bản thân với tối ưu hóa vi mô.


Là một câu hỏi phỏng vấn liên quan đến tối ưu hóa, mã này gây ra cuộc thảo luận thông thường nhưng lại bỏ lỡ mục tiêu cấp cao hơn của Mã có đúng về mặt chức năng không?

Cả C ++ và C và những người khác đều coi inttràn là một vấn đề từ a + b. Nó không được xác định rõ và C gọi đó là hành vi không xác định . Nó không được chỉ định để "bọc" - mặc dù đó là hành vi phổ biến.

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

Một hàm được gọi như vậy IsSumInRange()sẽ được dự kiến ​​sẽ được xác định rõ và thực hiện chính xác cho tất cả các intgiá trị của a,b. Nguyên a + bkhông có. Giải pháp AC có thể sử dụng:

#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;
}

Mã trên có thể được tối ưu hóa bằng cách sử dụng loại số nguyên rộng hơn int, nếu có, như dưới đây hoặc phân phối sum > N, sum < -Nkiểm tra trong if (a >= 0)logic. Tuy nhiên, việc tối ưu hóa như vậy có thể không thực sự dẫn đến mã phát ra "nhanh hơn" được cung cấp cho trình biên dịch thông minh cũng như không đáng để duy trì thêm sự thông minh.

  long long sum a;
  sum += b;

Ngay cả việc sử dụng abs(sum)cũng dễ gặp vấn đề khi sum == INT_MIN.


0

Chúng ta đang nói về loại trình biên dịch nào và loại "bộ nhớ" nào? Bởi vì trong ví dụ của bạn, giả sử một trình tối ưu hóa hợp lý, biểu thức a+bnói chung cần được lưu trữ trong một thanh ghi (một dạng bộ nhớ) trước khi thực hiện số học như vậy.

Vì vậy, nếu chúng ta đang nói về một trình biên dịch câm gặp a+bhai lần, thì nó sẽ phân bổ nhiều thanh ghi (bộ nhớ) hơn trong ví dụ thứ hai của bạn , bởi vì ví dụ đầu tiên của bạn có thể chỉ lưu trữ biểu thức đó một lần trong một thanh ghi duy nhất được ánh xạ tới biến cục bộ, nhưng chúng tôi Tại thời điểm này, chúng ta đang nói về các trình biên dịch rất ngớ ngẩn ... trừ khi bạn đang làm việc với một loại trình biên dịch ngớ ngẩn khác, ngăn chặn mọi biến số ở khắp mọi nơi, trong trường hợp đó có thể là trình biên dịch đầu tiên sẽ khiến nó đau buồn hơn để tối ưu hóa hơn thư hai*.

Tôi vẫn muốn xem xét điều đó và nghĩ rằng cái thứ hai có khả năng sử dụng nhiều bộ nhớ hơn với trình biên dịch câm ngay cả khi nó có xu hướng chồng chất tràn, bởi vì nó có thể sẽ phân bổ ba thanh ghi cho a+bvà tràn ra abhơn thế nữa. Nếu chúng ta đang nói ưu nguyên thủy nhất sau đó chụp a+bđể scó thể sẽ "giúp" nó sử dụng ít thanh ghi / stack tràn.

Đây hoàn toàn là những suy đoán theo những cách khá ngớ ngẩn khi đo / tháo gỡ và ngay cả trong những tình huống xấu nhất, đây không phải là trường hợp "bộ nhớ so với hiệu suất" (bởi vì ngay cả trong số những tối ưu hóa tồi tệ nhất tôi có thể nghĩ đến, chúng tôi không nói chuyện về bất cứ thứ gì ngoại trừ bộ nhớ tạm thời như stack / register), đó hoàn toàn là trường hợp "hiệu năng", và trong số bất kỳ trình tối ưu hóa hợp lý nào thì cả hai đều tương đương, và nếu một người không sử dụng trình tối ưu hóa hợp lý, tại sao lại ám ảnh về việc tối ưu hóa trong kính hiển vi và đặc biệt vắng mặt đo? Điều đó giống như lựa chọn hướng dẫn / đăng ký tập trung cấp độ tập hợp cấp độ mà tôi sẽ không bao giờ mong đợi bất cứ ai muốn duy trì năng suất làm việc khi sử dụng, giả sử, một trình thông dịch ngăn xếp mọi thứ.

Khi nào cần tối ưu hóa cho bộ nhớ so với tốc độ hiệu năng cho một phương thức?

Đối với câu hỏi này nếu tôi có thể giải quyết nó rộng hơn, thường thì tôi không tìm thấy hai đối lập về mặt đường kính. Đặc biệt là nếu các mẫu truy cập của bạn là tuần tự và với tốc độ của bộ đệm CPU, thường sẽ giảm số lượng byte được xử lý tuần tự cho các đầu vào không tầm thường dịch (lên đến một điểm) để lướt qua dữ liệu đó nhanh hơn. Tất nhiên, có những điểm đột phá trong đó nếu dữ liệu đổi nhiều hơn, nhỏ hơn nhiều để đổi lấy cách thức, hướng dẫn nhiều hơn, có thể nhanh hơn để xử lý tuần tự ở dạng lớn hơn để đổi lấy ít hướng dẫn hơn.

Nhưng tôi đã tìm thấy nhiều nhà phát triển có xu hướng đánh giá thấp mức độ giảm sử dụng bộ nhớ trong các loại trường hợp này có thể chuyển thành giảm tỷ lệ trong thời gian xử lý. Thật trực quan khi dịch các chi phí hiệu suất thành các hướng dẫn thay vì truy cập bộ nhớ đến điểm tiếp cận các LUT lớn trong một số nỗ lực vô ích để tăng tốc một số tính toán nhỏ, chỉ để thấy hiệu suất bị giảm khi truy cập bộ nhớ bổ sung.

Đối với các trường hợp truy cập tuần tự thông qua một số mảng lớn (không nói các biến vô hướng cục bộ như trong ví dụ của bạn), tôi đi theo quy tắc rằng ít bộ nhớ hơn để cày tuần tự chuyển thành hiệu suất cao hơn, đặc biệt là khi mã kết quả đơn giản hơn so với khác, cho đến khi nó không Tuy nhiên, cho đến khi các phép đo và trình lược tả của tôi cho tôi biết khác, và nó cũng quan trọng, giống như cách tôi giả sử đọc tuần tự một tệp nhị phân nhỏ hơn trên đĩa sẽ nhanh hơn để cày qua hơn một tệp lớn hơn (ngay cả khi tệp nhỏ hơn yêu cầu thêm một số hướng dẫn ), cho đến khi giả định đó được hiển thị không còn áp dụng trong các phép đo của tôi.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.