草庐IT

c++ - 在这 3 种从共享内存读取链表的方法中,为什么第三快?

coder 2023-11-16 原文

我有一个“服务器”程序,可以更新共享内存中的许多链表以响应外部事件。我希望客户端程序尽快注意到任何列表的更新(最低延迟)。一旦链表的数据被填充并且其下一个指针已设置为有效位置,服务器会将链表节点的 state_ 标记为 FILLED。在此之前,它的 state_NOT_FILLED_YET。我正在使用内存屏障来确保在内部数据实际准备好之前,客户端不会将 state_ 视为 FILLED(而且它似乎有效,我从未见过损坏数据)。此外,state_ 是易变的,以确保编译器不会解除客户端对其的检查,使其脱离循环。

保持服务器代码完全相同,我想出了 3 种不同的方法让客户端扫描链接列表以查找更改。问题是:为什么第三种方法最快?

方法 1:连续循环遍历所有链表(称为“ channel ”),查看是否有任何节点已更改为“已填充”:

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

当 channel 数量较少时,方法 1 的延迟非常低。但是当 channel 数量增加(250K+)时,由于遍历所有 channel ,它变得非常慢。所以我尝试...

方法二:给每个链表一个ID。在旁边保留一个单独的“更新列表”。每次更新其中一个链表时,将其 ID 推送到更新列表。现在我们只需要监控单个更新列表,并检查我们从中获取的 ID。

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

方法 2 产生了可怕的延迟。方法 1 可能会产生不到 10 微秒的延迟,而方法 2 会莫名其妙地经常产生 8 毫秒的延迟!使用 gettimeofday 似乎 update_cursor->state_ 的变化从服务器的 View 传播到客户端的 View 非常缓慢(我在多核机器上,所以我假设延迟是由于缓存造成的)。所以我尝试了一种混合方法...

方法三:保留更新列表。但是连续遍历所有 channel ,并在每次迭代中检查更新列表是否已更新。如果有,请使用推到上面的数字。如果没有,请检查我们当前迭代到的 channel 。

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

此方法的延迟与方法 1 一样好,但可扩展到大量 channel 。问题是,我不知道为什么。只是为了解决问题:如果我取消注释“通过更新找到”部分,它会在每个延迟日志消息之间打印。这意味着只能在更新列表中找到东西!所以我不明白这种方法怎么会比方法2快。

生成随机字符串作为测试数据的完整可编译代码(需要 GCC 和 boost-1.41)位于:http://pastebin.com/0kuzm3Uf

更新:所有 3 种方法都有效地自旋锁定,直到发生更新。不同之处在于他们需要多长时间才能注意到更新已经发生。它们不断地对处理器征税,所以这并不能解释速度差异。我正在一台 4 核机器上测试,没有其他任何东西在运行,所以服务器和客户端没有什么可以竞争的。我什至制作了一个代码版本,其中更新发出条件信号并让客户端等待该条件——这对任何方法的延迟都没有帮助。

更新2:尽管有3种方法,但我一次只试过1种,所以只有1台服务器和1台客户端在竞争state_成员。

最佳答案

假设:方法 2 以某种方式阻止服务器写入更新。

除了处理器核心本身之外,您可以锤击的其中一件事就是连贯缓存。当您读取给定核心上的值时,该核心上的 L1 缓存必须获得对该缓存行的读取访问权限,这意味着它需要使任何其他缓存对该行的写入访问无效。反之亦然写入一个值。因此,这意味着您不断地在“写入”状态(在服务器核心的缓存中)和“读取”状态(在所有客户端核心的缓存中)之间来回切换缓存行。

x86 缓存性能的复杂性并不是我完全熟悉的东西,但看起来完全合理(至少在理论上)你正在做的事情是让三个不同的线程尽可能多地敲打这个内存位置读取访问请求大约会在服务器上造成拒绝服务攻击,有时会阻止服务器写入该缓存行几毫秒。

您可以做一个实验来检测这一点,方法是查看服务器实际将值写入更新列表所花费的时间,并查看那里是否存在与延迟相对应的延迟。

您也可以尝试从等式中移除缓存的实验,方法是在单个内核上运行所有内容,以便客户端和服务器线程从同一个 L1 缓存中提取内容。

关于c++ - 在这 3 种从共享内存读取链表的方法中,为什么第三快?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/2531572/

有关c++ - 在这 3 种从共享内存读取链表的方法中,为什么第三快?的更多相关文章

  1. ruby - 如何使用 Nokogiri 的 xpath 和 at_xpath 方法 - 2

    我正在学习如何使用Nokogiri,根据这段代码我遇到了一些问题:require'rubygems'require'mechanize'post_agent=WWW::Mechanize.newpost_page=post_agent.get('http://www.vbulletin.org/forum/showthread.php?t=230708')puts"\nabsolutepathwithtbodygivesnil"putspost_page.parser.xpath('/html/body/div/div/div/div/div/table/tbody/tr/td/div

  2. ruby - 如何从 ruby​​ 中的字符串运行任意对象方法? - 2

    总的来说,我对ruby​​还比较陌生,我正在为我正在创建的对象编写一些rspec测试用例。许多测试用例都非常基础,我只是想确保正确填充和返回值。我想知道是否有办法使用循环结构来执行此操作。不必为我要测试的每个方法都设置一个assertEquals。例如:describeitem,"TestingtheItem"doit"willhaveanullvaluetostart"doitem=Item.new#HereIcoulddotheitem.name.shouldbe_nil#thenIcoulddoitem.category.shouldbe_nilendend但我想要一些方法来使用

  3. ruby - 为什么我可以在 Ruby 中使用 Object#send 访问私有(private)/ protected 方法? - 2

    类classAprivatedeffooputs:fooendpublicdefbarputs:barendprivatedefzimputs:zimendprotecteddefdibputs:dibendendA的实例a=A.new测试a.foorescueputs:faila.barrescueputs:faila.zimrescueputs:faila.dibrescueputs:faila.gazrescueputs:fail测试输出failbarfailfailfail.发送测试[:foo,:bar,:zim,:dib,:gaz].each{|m|a.send(m)resc

  4. ruby-on-rails - Ruby net/ldap 模块中的内存泄漏 - 2

    作为我的Rails应用程序的一部分,我编写了一个小导入程序,它从我们的LDAP系统中吸取数据并将其塞入一个用户表中。不幸的是,与LDAP相关的代码在遍历我们的32K用户时泄漏了大量内存,我一直无法弄清楚如何解决这个问题。这个问题似乎在某种程度上与LDAP库有关,因为当我删除对LDAP内容的调用时,内存使用情况会很好地稳定下来。此外,不断增加的对象是Net::BER::BerIdentifiedString和Net::BER::BerIdentifiedArray,它们都是LDAP库的一部分。当我运行导入时,内存使用量最终达到超过1GB的峰值。如果问题存在,我需要找到一些方法来更正我的代

  5. ruby - Facter::Util::Uptime:Module 的未定义方法 get_uptime (NoMethodError) - 2

    我正在尝试设置一个puppet节点,但ruby​​gems似乎不正常。如果我通过它自己的二进制文件(/usr/lib/ruby/gems/1.8/gems/facter-1.5.8/bin/facter)在cli上运行facter,它工作正常,但如果我通过由ruby​​gems(/usr/bin/facter)安装的二进制文件,它抛出:/usr/lib/ruby/1.8/facter/uptime.rb:11:undefinedmethod`get_uptime'forFacter::Util::Uptime:Module(NoMethodError)from/usr/lib/ruby

  6. ruby-on-rails - Rails - 子类化模型的设计模式是什么? - 2

    我有一个模型:classItem项目有一个属性“商店”基于存储的值,我希望Item对象对特定方法具有不同的行为。Rails中是否有针对此的通用设计模式?如果方法中没有大的if-else语句,这是如何干净利落地完成的? 最佳答案 通常通过Single-TableInheritance. 关于ruby-on-rails-Rails-子类化模型的设计模式是什么?,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.co

  7. ruby - 如何将脚本文件的末尾读取为数据文件(Perl 或任何其他语言) - 2

    我正在寻找执行以下操作的正确语法(在Perl、Shell或Ruby中):#variabletoaccessthedatalinesappendedasafileEND_OF_SCRIPT_MARKERrawdatastartshereanditcontinues. 最佳答案 Perl用__DATA__做这个:#!/usr/bin/perlusestrict;usewarnings;while(){print;}__DATA__Texttoprintgoeshere 关于ruby-如何将脚

  8. Ruby 方法() 方法 - 2

    我想了解Ruby方法methods()是如何工作的。我尝试使用“ruby方法”在Google上搜索,但这不是我需要的。我也看过ruby​​-doc.org,但我没有找到这种方法。你能详细解释一下它是如何工作的或者给我一个链接吗?更新我用methods()方法做了实验,得到了这样的结果:'labrat'代码classFirstdeffirst_instance_mymethodenddefself.first_class_mymethodendendclassSecond使用类#returnsavailablemethodslistforclassandancestorsputsSeco

  9. ruby - 什么是填充的 Base64 编码字符串以及如何在 ruby​​ 中生成它们? - 2

    我正在使用的第三方API的文档状态:"[O]urAPIonlyacceptspaddedBase64encodedstrings."什么是“填充的Base64编码字符串”以及如何在Ruby中生成它们。下面的代码是我第一次尝试创建转换为Base64的JSON格式数据。xa=Base64.encode64(a.to_json) 最佳答案 他们说的padding其实就是Base64本身的一部分。它是末尾的“=”和“==”。Base64将3个字节的数据包编码为4个编码字符。所以如果你的输入数据有长度n和n%3=1=>"=="末尾用于填充n%

  10. ruby - 解析 RDFa、微数据等的最佳方式是什么,使用统一的模式/词汇(例如 schema.org)存储和显示信息 - 2

    我主要使用Ruby来执行此操作,但到目前为止我的攻击计划如下:使用gemsrdf、rdf-rdfa和rdf-microdata或mida来解析给定任何URI的数据。我认为最好映射到像schema.org这样的统一模式,例如使用这个yaml文件,它试图描述数据词汇表和opengraph到schema.org之间的转换:#SchemaXtoschema.orgconversion#data-vocabularyDV:name:namestreet-address:streetAddressregion:addressRegionlocality:addressLocalityphoto:i

随机推荐