Tôi nên sử dụng vùng chứa STL nào cho FIFO?


89

Hộp chứa STL nào phù hợp với nhu cầu của tôi nhất? Về cơ bản, tôi có một vùng chứa rộng 10 phần tử, trong đó tôi liên tục nhập push_backcác phần tử mới trong khi pop_frontnhập phần tử cũ nhất (khoảng một triệu lần).

Tôi hiện đang sử dụng a std::dequecho nhiệm vụ nhưng đã tự hỏi liệu a std::listsẽ hiệu quả hơn vì tôi không cần phải phân bổ lại chính nó (hoặc có thể tôi đang nhầm a std::dequevới a std::vector?). Hoặc có một thùng chứa hiệu quả hơn cho nhu cầu của tôi?

Tái bút tôi không cần truy cập ngẫu nhiên


5
Tại sao không thử với cả hai và thời gian để xem cái nào nhanh hơn cho nhu cầu của bạn?
KTC

5
Tôi đã định làm điều này, nhưng tôi cũng đang tìm kiếm một câu trả lời lý thuyết.
Gab Royer,

2
những std::dequesẽ không phân bổ lại. Nó là sự kết hợp giữa a std::listvà a std::vectortrong đó nó phân bổ các khối lớn hơn a std::listnhưng sẽ không phân bổ lại như a std::vector.
Matt Price

2
Không, đây là đảm bảo có liên quan từ tiêu chuẩn: "Việc chèn một phần tử duy nhất vào đầu hoặc cuối của deque luôn mất thời gian không đổi và gây ra một lệnh gọi đến hàm tạo bản sao của T."
Matt Price

1
@John: Không, nó phân bổ lại. Có lẽ chúng ta chỉ đang trộn lẫn các thuật ngữ. Tôi nghĩ rằng phân bổ lại có nghĩa là lấy phân bổ cũ, sao chép nó vào một phân bổ mới và loại bỏ phân bổ cũ.
GManNickG

Câu trả lời:


194

Vì có vô số câu trả lời, bạn có thể nhầm lẫn, nhưng tóm lại:

Sử dụng a std::queue. Lý do cho điều này rất đơn giản: nó là một cấu trúc FIFO. Bạn muốn FIFO, bạn sử dụng a std::queue.

Nó làm cho ý định của bạn rõ ràng với bất kỳ ai khác, và ngay cả với chính bạn. A std::listhoặc std::dequekhông. Một danh sách có thể chèn và xóa ở bất kỳ đâu, đây không phải là điều mà cấu trúc FIFO được cho là làm và dequecó thể thêm và xóa từ một trong hai đầu, đây cũng là điều mà cấu trúc FIFO không thể làm được.

Đây là lý do tại sao bạn nên sử dụng a queue.

Bây giờ, bạn đã hỏi về hiệu suất. Đầu tiên, hãy luôn nhớ quy tắc ngón tay cái quan trọng này: Mã tốt trước, hiệu suất sau cùng.

Lý do cho điều này rất đơn giản: những người phấn đấu để đạt được hiệu suất trước sự sạch sẽ và thanh lịch hầu như luôn về đích cuối cùng. Mã của họ trở thành một mớ hỗn độn, bởi vì họ đã từ bỏ tất cả những gì tốt đẹp để thực sự không nhận được gì từ nó.

Bằng cách viết mã tốt, dễ đọc trước, hầu hết các vấn đề về hiệu suất sẽ tự giải quyết. Và nếu sau đó, bạn thấy hiệu suất của mình bị thiếu, bây giờ bạn có thể dễ dàng thêm một bộ hồ sơ vào mã sạch đẹp của mình và tìm ra vấn đề ở đâu.

Điều đó đã nói tất cả, std::queuechỉ là một bộ chuyển đổi. Nó cung cấp giao diện an toàn, nhưng sử dụng một vùng chứa khác ở bên trong. Bạn có thể chọn vùng chứa bên dưới này và điều này cho phép rất linh hoạt.

Vì vậy, bạn nên sử dụng vùng chứa bên dưới nào? Chúng ta biết rằng std::liststd::dequecả cung cấp các chức năng cần thiết ( push_back(), pop_front()front()), vì vậy làm thế nào để chúng tôi quyết định?

Trước tiên, hãy hiểu rằng phân bổ (và phân bổ) bộ nhớ nói chung không phải là một việc nhanh chóng để làm, bởi vì nó liên quan đến việc ra ngoài hệ điều hành và yêu cầu nó làm một việc gì đó. A listphải cấp phát bộ nhớ mỗi khi một cái gì đó được thêm vào và phân bổ nó khi nó biến mất.

A deque, mặt khác, phân bổ theo khối. Nó sẽ phân bổ ít thường xuyên hơn a list. Hãy coi nó như một danh sách, nhưng mỗi đoạn bộ nhớ có thể chứa nhiều nút. (Tất nhiên, tôi khuyên bạn nên thực sự tìm hiểu cách hoạt động của nó .)

Vì vậy, chỉ với điều đó một mình dequesẽ hoạt động tốt hơn, bởi vì nó không liên quan đến bộ nhớ thường xuyên. Kết hợp với thực tế là bạn đang xử lý dữ liệu có kích thước không đổi, nó có thể sẽ không phải phân bổ sau lần đầu tiên chuyển qua dữ liệu, trong khi một danh sách sẽ liên tục phân bổ và phân bổ.

Điều thứ hai cần hiểu là hiệu suất bộ nhớ cache . Việc sử dụng RAM rất chậm, vì vậy khi CPU thực sự cần, nó sẽ tận dụng tối đa khoảng thời gian này bằng cách lấy lại một phần bộ nhớ vào bộ nhớ đệm. Vì một dequephân bổ trong các khối bộ nhớ, có khả năng việc truy cập một phần tử trong vùng chứa này cũng sẽ khiến CPU đưa phần còn lại của vùng chứa trở lại. Giờ đây, bất kỳ truy cập nào tiếp theo vào dequesẽ diễn ra nhanh chóng, bởi vì dữ liệu nằm trong bộ nhớ cache.

Điều này không giống như một danh sách, nơi dữ liệu được phân bổ từng lần một. Điều này có nghĩa là dữ liệu có thể được dàn trải khắp nơi trong bộ nhớ và hiệu suất bộ nhớ cache sẽ kém.

Vì vậy, xét rằng, a dequenên là một lựa chọn tốt hơn. Đây là lý do tại sao nó là vùng chứa mặc định khi sử dụng a queue. Điều đó đã nói lên tất cả, đây vẫn chỉ là một phỏng đoán (rất) được giáo dục: bạn sẽ phải lập hồ sơ mã này, sử dụng một dequetrong một bài kiểm tra và listtrong một bài kiểm tra khác để thực sự biết chắc chắn.

Nhưng hãy nhớ: để mã hoạt động với giao diện sạch sẽ, sau đó lo lắng về hiệu suất.

John lo ngại rằng việc bao bọc một listhoặc dequesẽ làm giảm hiệu suất. Một lần nữa, anh ấy và tôi có thể nói chắc chắn mà không cần tự mình lược tả nó, nhưng rất có thể là trình biên dịch sẽ nội tuyến các lệnh gọi mà nó queuethực hiện. Có nghĩa là, khi bạn nói queue.push(), nó thực sự sẽ chỉ nói queue.container.push_back(), bỏ qua hoàn toàn lệnh gọi hàm.

Một lần nữa, đây chỉ là một phỏng đoán có học thức, nhưng sử dụng một queuesẽ không làm giảm hiệu suất, khi so sánh với việc sử dụng nguyên container bên dưới. Như tôi đã nói trước đây, hãy sử dụng queue, bởi vì nó sạch sẽ, dễ sử dụng và an toàn, và nếu nó thực sự trở thành một hồ sơ vấn đề và thử nghiệm.


10
+1 - và nếu hóa ra boost :: circle_buffer <> có hiệu suất tốt nhất, thì chỉ cần sử dụng nó làm vùng chứa bên dưới (nó cũng cung cấp push_back (), pop_front (), front () và back () ).
Michael Burr

2
Được chấp nhận vì đã giải thích chi tiết (đó là điều tôi cần, cảm ơn vì đã dành thời gian). Đối với hiệu suất đầu tiên mã tốt cuối cùng, tôi phải thừa nhận đó là một trong những mặc định lớn nhất của tôi, tôi luôn cố gắng làm mọi thứ một cách hoàn hảo trong lần chạy đầu tiên ... Tôi đã viết mã bằng cách sử dụng deque khó khăn đầu tiên, nhưng vì điều đó đã không ' không hoạt động tốt như tôi nghĩ (nó được cho là gần như thời gian thực), tôi đoán tôi nên cải thiện nó một chút. Như Neil cũng đã nói, tôi thực sự nên sử dụng một bộ hồ sơ ... Mặc dù tôi rất vui vì đã mắc phải những lỗi này trong khi nó không thực sự quan trọng. Cảm ơn rất nhiều tất cả các bạn.
Gab Royer

4
-1 vì không giải quyết được vấn đề và câu trả lời vô ích cồng kềnh. Câu trả lời đúng ở đây là ngắn gọn và nó là boost :: circle_buffer <>.
Dmitry Chichkov

1
"Tốt mã đầu tiên, hiệu suất cuối cùng", đó là một câu nói tuyệt vời. Giá mà mọi người hiểu điều này :)
thegreendroid

Tôi đánh giá cao sự căng thẳng trong việc lập hồ sơ. Cung cấp một quy tắc ngón tay cái là một chuyện và sau đó chứng minh điều đó bằng việc lập hồ sơ là một điều tốt hơn
storykeDskobeDa

28

Kiểm tra std::queue. Nó bao bọc một loại vùng chứa bên dưới và vùng chứa mặc định là std::deque.


3
Mọi lớp thừa sẽ bị trình biên dịch loại bỏ. Theo logic của bạn, tất cả chúng ta chỉ nên lập trình trong hợp ngữ, vì ngôn ngữ chỉ là một cái vỏ cản trở. Vấn đề là sử dụng đúng loại cho công việc. Và queuelà loại đó. Mã tốt trước, hiệu suất sau. Chết tiệt, hầu hết hiệu suất đến từ việc sử dụng mã tốt ngay từ đầu.
GManNickG

2
Xin lỗi vì đã mơ hồ - quan điểm của tôi là một hàng đợi chính xác là những gì câu hỏi yêu cầu và các nhà thiết kế C ++ nghĩ rằng deque là một vùng chứa cơ bản tốt cho trường hợp sử dụng này.
Mark Ransom

2
Không có gì trong câu hỏi này để chỉ ra rằng anh ấy thấy thiếu hiệu suất. Nhiều người mới bắt đầu liên tục hỏi về giải pháp hiệu quả nhất cho bất kỳ vấn đề nào, bất kể giải pháp hiện tại của họ có hoạt động tốt hay không.
jalf

1
@John, nếu anh ấy thấy hiệu suất thiếu, việc loại bỏ lớp vỏ an toàn queuesẽ không làm tăng hiệu suất, như tôi đã nói. Bạn đã đề xuất một list, có thể sẽ hoạt động kém hơn.
GManNickG

3
Vấn đề về std :: queue <> là nếu deque <> không phải là thứ bạn muốn (vì hiệu suất hoặc bất cứ lý do gì), bạn nên thay đổi nó để sử dụng std :: list làm kho dự trữ - as GMan nói đường lui. Và nếu bạn thực sự muốn sử dụng bộ đệm vòng thay vì danh sách, boost :: circle_buffer <> sẽ xuất hiện ngay trong ... std :: queue <> gần như chắc chắn là 'giao diện' nên được sử dụng. Kho hỗ trợ cho nó có thể được thay đổi khá nhiều theo ý muốn.
Michael Burr


7

Tôi liên tục nhập push_backcác phần tử mới trong khi pop_frontnhập phần tử cũ nhất (khoảng một triệu lần).

Một triệu thực sự không phải là một con số lớn trong lĩnh vực điện toán. Như những người khác đã đề xuất, hãy sử dụng một std::queuegiải pháp đầu tiên của bạn. Trong trường hợp không thể xảy ra là nó quá chậm, hãy xác định nút cổ chai bằng cách sử dụng một trình mô tả (đừng đoán!) Và triển khai lại bằng cách sử dụng một vùng chứa khác có cùng giao diện.


1
Vấn đề là, đó là một con số lớn vì những gì tôi muốn làm phải là thời gian thực. Mặc dù bạn là đúng rằng tôi nên đã sử dụng một hồ sơ để xác định nguyên nhân ...
Gab Royer

Vấn đề là, tôi không thực sự quen với việc sử dụng một hồ sơ (chúng tôi đã sử dụng gprof một chút trong một trong các lớp của mình nhưng chúng tôi thực sự không đi sâu ...). Nếu bạn có thể chỉ cho tôi một số nguồn thông tin, tôi sẽ đánh giá rất cao! Tái bút. Tôi sử dụng VS2008
Gab Royer

@Gab: Bạn có VS2008 nào (Express, Pro ...)? Một số đi kèm với một hồ sơ.
sbi

@Gab Xin lỗi, tôi không sử dụng VS nữa như vậy có thể không thực sự tham mưu

@Sbi, từ những gì tôi đang thấy, nó chỉ có trong phiên bản hệ thống nhóm (mà tôi có quyền truy cập). Tôi sẽ xem xét điều này.
Gab Royer

5

Tại sao không std::queue? Tất cả những gì nó có là push_backpop_front.


3

Một hàng đợi có lẽ là một giao diện đơn giản hơn một deque nhưng đối với một danh sách nhỏ như vậy, sự khác biệt trong hoạt động có lẽ là không đáng kể.

Tương tự với danh sách . Bạn chỉ cần lựa chọn API nào bạn muốn.


Nhưng tôi đã tự hỏi nếu push_back liên tục đã làm cho hàng đợi hoặc tái phân bổ deque mình
Gab Royer

std :: queue là một trình bao bọc xung quanh một vùng chứa khác, vì vậy một hàng đợi bao bọc một deque sẽ kém hiệu quả hơn một deque thô.
John Millikin

1
Đối với 10 mục, hiệu suất rất có thể sẽ là một vấn đề nhỏ như vậy, "hiệu quả" có thể tốt hơn được đo bằng thời gian lập trình viên hơn là thời gian viết mã. Và các cuộc gọi từ hàng đợi đến hàng đợi bởi bất kỳ tối ưu hóa trình biên dịch tốt nào sẽ trở nên vô ích.
lavinio

2
@John: Tôi muốn bạn cho tôi xem một bộ tiêu chuẩn thể hiện sự khác biệt về hiệu suất như vậy. Nó không kém hiệu quả hơn deque thô. Các trình biên dịch C ++ nội tuyến rất mạnh mẽ.
jalf

3
Tôi đã thử nó. : Vùng chứa 10 phần tử DA nhanh & bẩn với 100.000.000 số int pop_front () & push_back () rand () trên Bản dựng phát hành cho tốc độ trên VC9 cho: list (27), queue (6), deque (6), array (8) .
KTC

0

Sử dụng a std::queue, nhưng phải hiểu rõ về sự cân bằng hiệu suất của hai Containerlớp tiêu chuẩn .

Theo mặc định, std::queuelà một bộ điều hợp trên đầu trang std::deque. Thông thường, điều đó sẽ mang lại hiệu suất tốt khi bạn có một số lượng nhỏ hàng đợi chứa một số lượng lớn các mục nhập, đây được cho là trường hợp phổ biến.

Tuy nhiên, đừng mù quáng với việc triển khai std :: deque . Đặc biệt:

"... deque thường có chi phí bộ nhớ tối thiểu lớn; một deque chỉ giữ một phần tử phải phân bổ toàn bộ mảng bên trong của nó (ví dụ: 8 lần kích thước đối tượng trên 64-bit libstdc ++; 16 lần kích thước đối tượng hoặc 4096 byte, tùy theo giá trị nào lớn hơn , trên libc ++ 64-bit). "

Để loại bỏ điều đó, giả sử rằng mục nhập hàng đợi là thứ mà bạn muốn xếp hàng, tức là có kích thước nhỏ hợp lý, thì nếu bạn có 4 hàng đợi, mỗi hàng chứa 30.000 mục nhập, thì việc std::dequetriển khai sẽ là tùy chọn được lựa chọn. Ngược lại, nếu bạn có 30.000 hàng đợi, mỗi hàng có 4 mục nhập, thì nhiều khả năng việc std::listtriển khai sẽ tối ưu, vì bạn sẽ không bao giờ phân bổ std::dequechi phí trong trường hợp đó.

Bạn sẽ đọc rất nhiều ý kiến ​​về việc bộ nhớ cache là vua như thế nào, Stroustrup ghét danh sách liên kết như thế nào, v.v. và tất cả những điều đó đều đúng, trong những điều kiện nhất định. Chỉ cần không chấp nhận nó với niềm tin mù quáng, bởi vì trong kịch bản thứ hai của chúng tôi ở đó, rất khó có khả năng std::dequetriển khai mặc định sẽ hoạt động. Đánh giá việc sử dụng và đo lường của bạn.


-1

Trường hợp này đủ đơn giản để bạn có thể tự viết. Đây là một cái gì đó hoạt động tốt cho các trường hợp conroller vi mô trong đó việc sử dụng STL chiếm quá nhiều không gian. Đó là một cách hay để truyền dữ liệu và tín hiệu từ trình xử lý ngắt đến vòng lặp chính của bạn.

// FIFO with circular buffer
#define fifo_size 4

class Fifo {
  uint8_t buff[fifo_size];
  int writePtr = 0;
  int readPtr = 0;
  
public:  
  void put(uint8_t val) {
    buff[writePtr%fifo_size] = val;
    writePtr++;
  }
  uint8_t get() {
    uint8_t val = NULL;
    if(readPtr < writePtr) {
      val = buff[readPtr%fifo_size];
      readPtr++;
      
      // reset pointers to avoid overflow
      if(readPtr > fifo_size) {
        writePtr = writePtr%fifo_size;
        readPtr = readPtr%fifo_size;
      }
    }
    return val;
  }
  int count() { return (writePtr - readPtr);}
};

Nhưng làm thế nào / khi nào điều đó sẽ xảy ra?
user10658782

Ồ, tôi nghĩ nó có thể vì một số lý do. Đừng bận tâm!
Ry-
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.