Tôi có chương trình 'máy chủ' cập nhật nhiều danh sách được liên kết trong bộ nhớ dùng chung để phản hồi các sự kiện bên ngoài. Tôi muốn các chương trình khách hàng nhận thấy một bản cập nhật trên bất kỳ danh sách nào càng nhanh càng tốt (độ trễ thấp nhất). Máy chủ đánh dấu nút của một danh sách liên kết là state_
là FILLED
khi dữ liệu của nó được điền và con trỏ tiếp theo của nó đã được đặt thành vị trí hợp lệ. Cho đến lúc đó, state_
của nó là NOT_FILLED_YET
. Tôi đang sử dụng các rào cản bộ nhớ để đảm bảo rằng khách hàng không thấy state_
là FILLED
trước khi dữ liệu bên trong thực sự sẵn sàng (và có vẻ như nó hoạt động, tôi không bao giờ thấy dữ liệu bị hỏng). Ngoài ra, state_
là dễ bay hơi để đảm bảo trình biên dịch không nâng kiểm tra của khách hàng của nó ra khỏi vòng lặp.Trong 3 phương pháp này để đọc danh sách được liên kết từ bộ nhớ dùng chung, tại sao lại là 3 nhanh nhất?
Giữ mã máy chủ giống hệt nhau, tôi đã đưa ra 3 phương pháp khác nhau để khách hàng quét danh sách được liên kết để thay đổi. Câu hỏi đặt ra là: Tại sao phương pháp thứ 3 nhanh nhất?
Phương pháp 1: robin tròn trên tất cả các danh sách liên kết (gọi là 'kênh') liên tục, tìm kiếm để xem nếu có các nút đã thay đổi để 'ĐIỀN':
void method_one()
{
std::vector<Data*> channel_cursors;
for(ChannelList::iterator i = channel_list.begin(); i != channel_list.end(); ++i)
{
Data* current_item = static_cast<Data*>(i->get(segment)->tail_.get(segment));
channel_cursors.push_back(current_item);
}
while(true)
{
for(std::size_t i = 0; i < channel_list.size(); ++i)
{
Data* current_item = channel_cursors[i];
ACQUIRE_MEMORY_BARRIER;
if(current_item->state_ == NOT_FILLED_YET) {
continue;
}
log_latency(current_item->tv_sec_, current_item->tv_usec_);
channel_cursors[i] = static_cast<Data*>(current_item->next_.get(segment));
}
}
}
Phương pháp 1 cho độ trễ rất thấp khi sau đó số lượng kênh nhỏ. Nhưng khi số lượng kênh tăng lên (250K +) thì nó trở nên rất chậm vì lặp lại tất cả các kênh. Vì vậy, tôi đã thử ...
Cách 2: Cung cấp cho mỗi danh sách liên kết một ID. Giữ một 'danh sách cập nhật' riêng biệt sang một bên. Mỗi khi một trong các danh sách liên kết được cập nhật, hãy đẩy ID của nó vào danh sách cập nhật. Bây giờ chúng ta chỉ cần theo dõi danh sách cập nhật duy nhất, và kiểm tra các ID chúng ta nhận được từ nó.
void method_two()
{
std::vector<Data*> channel_cursors;
for(ChannelList::iterator i = channel_list.begin(); i != channel_list.end(); ++i)
{
Data* current_item = static_cast<Data*>(i->get(segment)->tail_.get(segment));
channel_cursors.push_back(current_item);
}
UpdateID* update_cursor = static_cast<UpdateID*>(update_channel.tail_.get(segment));
while(true)
{
ACQUIRE_MEMORY_BARRIER;
if(update_cursor->state_ == NOT_FILLED_YET) {
continue;
}
::uint32_t update_id = update_cursor->list_id_;
Data* current_item = channel_cursors[update_id];
if(current_item->state_ == NOT_FILLED_YET) {
std::cerr << "This should never print." << std::endl; // it doesn't
continue;
}
log_latency(current_item->tv_sec_, current_item->tv_usec_);
channel_cursors[update_id] = static_cast<Data*>(current_item->next_.get(segment));
update_cursor = static_cast<UpdateID*>(update_cursor->next_.get(segment));
}
}
Phương pháp 2 cho độ trễ TERRIBLE. Trong khi Phương pháp 1 có thể cho độ trễ dưới 10us, Phương pháp 2 sẽ không thể giải thích thường xuyên về độ trễ 8ms! Sử dụng gettimeofday nó xuất hiện rằng sự thay đổi trong update_cursor-> state_ rất chậm để chuyển từ chế độ xem của máy chủ sang máy khách (tôi đang ở trên một hộp đa lõi, vì vậy tôi cho rằng sự chậm trễ là do bộ nhớ cache). Vì vậy, tôi đã thử một cách tiếp cận lai ...
Phương pháp 3: Giữ danh sách cập nhật. Nhưng lặp lại tất cả các kênh liên tục và trong mỗi lần kiểm tra lặp lại nếu danh sách cập nhật đã được cập nhật. Nếu nó có, đi với số đẩy lên nó. Nếu chưa, hãy kiểm tra kênh mà chúng tôi hiện đang lặp lại.
void method_three()
{
std::vector<Data*> channel_cursors;
for(ChannelList::iterator i = channel_list.begin(); i != channel_list.end(); ++i)
{
Data* current_item = static_cast<Data*>(i->get(segment)->tail_.get(segment));
channel_cursors.push_back(current_item);
}
UpdateID* update_cursor = static_cast<UpdateID*>(update_channel.tail_.get(segment));
while(true)
{
for(std::size_t i = 0; i < channel_list.size(); ++i)
{
std::size_t idx = i;
ACQUIRE_MEMORY_BARRIER;
if(update_cursor->state_ != NOT_FILLED_YET) {
//std::cerr << "Found via update" << std::endl;
i--;
idx = update_cursor->list_id_;
update_cursor = static_cast<UpdateID*>(update_cursor->next_.get(segment));
}
Data* current_item = channel_cursors[idx];
ACQUIRE_MEMORY_BARRIER;
if(current_item->state_ == NOT_FILLED_YET) {
continue;
}
found_an_update = true;
log_latency(current_item->tv_sec_, current_item->tv_usec_);
channel_cursors[idx] = static_cast<Data*>(current_item->next_.get(segment));
}
}
}
Độ trễ của phương pháp này tốt như Phương pháp 1, nhưng được chia tỷ lệ thành số lượng lớn kênh. Vấn đề là, tôi không biết tại sao. Chỉ cần ném một cờ lê vào mọi thứ: nếu tôi bỏ ghi chú phần 'tìm thấy thông qua cập nhật', nó sẽ in giữa MỌI LATENCY LOG MESSAGE. Có nghĩa là mọi thứ chỉ được tìm thấy trong danh sách cập nhật! Vì vậy, tôi không hiểu tại sao phương pháp này có thể nhanh hơn so với phương pháp 2.
Các đầy đủ, mã biên dịch được (yêu cầu GCC và tăng-1.41) mà tạo ra chuỗi ngẫu nhiên như dữ liệu thử nghiệm là tại địa chỉ: http://pastebin.com/0kuzm3Uf
Cập nhật: Tất cả 3 phương pháp có hiệu quả spinlocking cho đến khi một cập nhật xảy ra. Sự khác biệt là trong thời gian bao lâu để họ nhận thấy bản cập nhật đã xảy ra. Họ tất cả liên tục đánh thuế bộ vi xử lý, do đó không giải thích được sự khác biệt về tốc độ. Tôi đang thử nghiệm trên một máy tính 4 lõi không có gì khác chạy, vì vậy máy chủ và máy khách không có gì để cạnh tranh. Tôi thậm chí đã thực hiện một phiên bản của mã nơi cập nhật báo hiệu một điều kiện và có khách hàng chờ đợi về điều kiện - nó không giúp độ trễ của bất kỳ phương pháp nào.
Update2: Mặc dù có 3 phương pháp, tôi chỉ thử 1 lần, vì vậy chỉ có 1 máy chủ và 1 máy khách đang cạnh tranh cho thành viên state_.
Có, đó là để tránh điều kiện cuộc đua. Nó được cho là cũng ở phương pháp 2, tôi đã chỉnh sửa bài đăng của mình để sửa lỗi đó (thời gian giống nhau). Các bộ vi xử lý hiện đại (không chỉ trình biên dịch) được tự do sắp xếp lại các cửa hàng và tải trên các luồng, vì vậy một luồng có thể đặt A rồi B, nhưng một luồng khác có thể 'thấy' rằng B đã được đặt trước khi nó thấy A được đặt. Máy chủ điền dữ liệu của nút, thực hiện RELEASE_MEMORY_BARRIER, sau đó đặt state_ thành FILLED. Kết hợp với máy khách đang thực hiện ACQUIRE_MEMORY_BARRIER, điều này đảm bảo rằng máy khách không bao giờ cảm nhận state_ là FILLED trước khi dữ liệu của nút được điền vào. –
Tôi nên thêm rằng đây là lần đầu tiên tôi sử dụng các rào cản bộ nhớ, và đó là cách tôi * nghĩ * để làm việc;) Nếu bạn theo liên kết pastebin, tệp đầu tiên, danh sách.H định nghĩa RELEASE_MEMORY_BARRIER và ACQUIRE_MEMORY_BARRIER cho các tài liệu gốc được ghi lại tại đây: http://gcc.gnu.org/onlinedocs/gcc-4.1.2/gcc/Atomic-Builtins .html –
Xin lỗi vì spam, p Tôi đã chỉnh sửa bài đăng của mình (xem phần dưới) để giải quyết việc xử lý của bộ xử lý. –