1

So I am using yaml-cpp to be able to use yaml for my game data files in c++ however I am running into some major performance issues.

I wanted to test out a somewhat large file so I created some dummy data to write out:

Player newPlayer = Player();
newPlayer.name = "new player";
newPlayer.maximumHealth = 1000;
newPlayer.currentHealth = 1;

Inventory newInventory;
newInventory.maximumWeight = 10.9f;

for (int z = 0; z < 10000; z++) {
  InventoryItem* newItem = new InventoryItem();
  newItem->name = "Stone";
  newItem->baseValue = 1;
  newItem->weight = 0.1f;

  newInventory.items.push_back(newItem);
}

YAML::Node newSavedGame;
newSavedGame["player"] = newPlayer;
newSavedGame["inventory"] = newInventory;

I then wrote this function to be able to take data and write it out to a file:

void YamlUtility::saveAsFile(YAML::Node node, std::string filePath) {
  std::ofstream myfile;

  myfile.open(filePath);
  myfile << node << std::endl;

  myfile.close();
}

Now before I added this code, the memory usage of my game was at about 22MB. After I added the newPlayer, newInventory and the the InventoryItems it went to about 23MB. Then when I added in the YAML::Node newSavedGame, the memory went up to 108MB. Also the file that is written out is only 570KB so I can't think of why it would spike the memory up by like 85MB.

The second issue is that this code takes about 8 seconds to write the file. That just seemed a bit off to me.

I decide to rewrite the save function using YAML::Emitter, that code looks like this:

static void buildYamlManually(std::ofstream& file, YAML::Node node) {
  YAML::Emitter out;
  out << YAML::BeginMap << YAML::Key << "player" << YAML::Value << YAML::BeginMap << YAML::Key << "name" << YAML::Value
      << node["player"]["name"].as<std::string>() << YAML::Key << "maximumHealth" << YAML::Value
      << node["player"]["maximumHealth"].as<int>() << YAML::Key << "currentHealth" << YAML::Value
      << node["player"]["currentHealth"].as<int>() << YAML::EndMap;

  out << YAML::BeginSeq;

  std::vector<InventoryItem*> items = node["inventory"]["items"].as<std::vector<InventoryItem*>>();

  for (InventoryItem* const value : items) {
    out << YAML::BeginMap << YAML::Key << "name" << YAML::Value << value->name << YAML::Key << "baseValue"
        << YAML::Value << value->baseValue << YAML::Key << "weight" << YAML::Value << value->weight << YAML::EndMap;
  }

  out << YAML::EndSeq;

  out << YAML::EndMap;

  file << out.c_str() << std::endl;
}

This seemed to have a small effect on the performance however it was still closer to 7 seconds to save the file (instead of 8 seconds).

I then decided to just see what it would be like if I wrote the file manually without yaml-cpp at all, that code looks like this:

static void buildYamlManually(std::ofstream& file, SavedGame savedGame) {
  file << "player: \n"
       << "  name: " << savedGame.player.name << "\n  maximumHealth: " << savedGame.player.maximumHealth
       << "\n  currentHealth: " << savedGame.player.currentHealth << "\ninventory:"
       << "\n  maximumWeight: " << savedGame.inventory.maximumWeight << "\n  items:";

  for (InventoryItem* const value : savedGame.inventory.items) {
    file << "\n    - name: " << value->name << "\n      baseValue: " << value->baseValue
         << "\n      weight: " << value->weight;
  }
}

With this code and all yaml-cpp code removed, the memory went from 23MB to 24MB and writing the files took about 0.15 seconds.

While I would understand there being some overhead with using yaml-cpp vs dealing with the file manually just as text, this kind of performance difference just seems wrong.

I want to say I am doing something wrong but based on the yaml-cpp documentation, I can't see what that might be.

ryanzec
  • 27,284
  • 38
  • 112
  • 169
  • 1
    The standard advice is to use a profiler. But with delays that long, you can resort to the one-penny profiler, use your debugger's Break command. There ought to be some consistency in the stack frames you see back, clearly an excellent way to document your question better. – Hans Passant Jun 16 '18 at 22:05
  • Looks like you need to read a book on C++ - this: `Player newPlayer = Player();` is not how we do things. And neither is this: `InventoryItem* newItem = new InventoryItem();` –  Jun 16 '18 at 22:05
  • Is a YAML::Node expensive to copy? Can you pass a reference to your save function instead of a copy? – user1593858 Jun 16 '18 at 22:09
  • @user1593858 changing my code to pass the YAML::Node by reference does not effect the file save performance or the memory usage (the memory usage happen even if I don't save the file). – ryanzec Jun 16 '18 at 22:17
  • @NeilButterworth Well for the first thing, I assume you mean I can just do `Player newPlayer;` and the default constructor will be automatically called however while I can think a different way to do `InventoryItem* newItem = new InventoryItem();` I am not actually sure what is wrong with the current code. I see this pattern being used in Effective C++ Third Edition which is a book that seems to be popular. – ryanzec Jun 16 '18 at 22:36
  • or you mean I should be creating constructors for those structs that take in the data so I can set it when I initialize however I am just guessing based on your vague feedback. – ryanzec Jun 16 '18 at 22:38
  • Which part of Effective C++ (a very old book on C++, BTW) do you see that suggests using raw pointers to manage dynamic memory? You should be using smart pointers, such as std::unique_ptr, and allocating such memory with functions such as make_unique(). And yes, of course you should be providing constructors - I believe Effective C++ says exactly that. –  Jun 16 '18 at 22:39
  • As for the constructor, I just did not create it as this was quicker for me and is code just to test `yaml-cpp`, it is going to get thrown away after my testing. As for pointer management, I guess I should be using the std pointer management functionality. I am just coming back to C++ after about 12 - 13 years away from it and I guess I am still used to creating pointers manually instead of using things like `std::unique_ptr`, `std::shared_ptr`, etc – ryanzec Jun 16 '18 at 23:16

1 Answers1

2

You need to provide a complete example that actually demonstrates the problem. I've been meaning to try out yaml-cpp so this morning I tried to reproduce your problem but was unable to do so. Using the code below which is very similar to the snippets you provided, writing the file took ~0.06s in my VM. It looks like the problem is not inherent in yaml-cpp but rather somewhere in your code.

#include <string>
#include <vector>
#include <iostream>
#include <yaml-cpp/yaml.h>
#include <fstream>
#include <chrono>

class Player
{
    public:
        Player(const std::string& name, int maxHealth, int curHealth) : 
          m_name(name),
          m_maxHealth(maxHealth),
          m_currentHealth(curHealth) 
        {
        }

        const std::string& name() const     { return m_name;}
        int maxHealth() const               { return m_maxHealth; }
        int currentHealth() const           { return m_currentHealth; }

    private:
        const std::string m_name;
        int m_maxHealth;
        int m_currentHealth;
};

class Item
{
    public:
        Item(const std::string& name, int value, double weight) :
          m_name(name),
          m_value(value),
          m_weight(weight)
        {
        }

        const std::string& name() const     { return m_name; }
        int value() const                   { return m_value; }
        double maxWeight() const            { return m_weight; }

    private:
        const std::string m_name;
        int m_value;
        double m_weight;
};

class Inventory
{
    public:
        Inventory(double maxWeight) :
          m_maxWeight(maxWeight) 
        {
            m_items.reserve(10'000);
        }

        std::vector<Item>& items()              { return m_items;}
        const std::vector<Item>& items() const  { return m_items;}

        double maxWeight() const                { return m_maxWeight; };

    private:
        double m_maxWeight;
        std::vector<Item> m_items;
};

namespace YAML
{

    template<>
    struct convert<Inventory> 
    {
        static Node encode(const Inventory& rhs)
        {
            Node node;
            node.push_back(rhs.maxWeight());
            for(const auto& item : rhs.items())
            {
                node.push_back(item.name());
                node.push_back(item.value());
                node.push_back(item.maxWeight());
            }
            return node;
        }

        // TODO decode Inventory
    };


    template<>
    struct convert<Player> 
    {
        static Node encode(const Player& rhs)
        {
            Node node;
            node.push_back(rhs.name());
            node.push_back(rhs.maxHealth());
            node.push_back(rhs.currentHealth());
            return node;
        }

        //TODO Decode Player
    };

}

void saveAsFile(const YAML::Node& node, const std::string& filePath)
{
    std::ofstream myFile(filePath);

    myFile << node << std::endl;
}

int main(int arg, char **argv)
{
    Player newPlayer("new player", 1'000, 1);

    Inventory newInventory(10.9f);

    for(int z = 0; z < 10'000; z++)
    {
        newInventory.items().emplace_back("Stone", 1, 0.1f);
    }

    std::cout << "Inventory has " << newInventory.items().size() << " items\n";

    YAML::Node newSavedGame;
    newSavedGame["player"] = newPlayer;
    newSavedGame["inventory"] = newInventory;


    //Measure it 
    auto start = std::chrono::high_resolution_clock::now();

    saveAsFile(newSavedGame, "/tmp/save.yaml");

    auto end = std::chrono::high_resolution_clock::now();

    std::cout << "Wrote to file in " 
              << std::chrono::duration_cast<std::chrono::duration<double>>(end - start).count() 
              << " seconds\n";

    return 0;
}

Output:

user@mintvm ~/Desktop/yaml $ g++ -std=c++14 -o test main.cpp -lyaml-cpp
user@mintvm ~/Desktop/yaml $ ./test 
Inventory has 10000 items
Wrote to file in 0.0628495 second

Update edit (by Michael Goldshteyn):

I wanted to run this on a native machine rather than a VM to show that in fact the above code was even faster when built with proper optimizations, timed properly, and run native (i.e., not in a VM):

$ # yaml-cpp built from source commit: * c90c08cThu Aug 9 10:05:07 2018 -0500 
$ #   (HEAD -> master, origin/master, origin/HEAD)
$ #   Revert "Improvements to CMake buildsystem (#563)"
$ #  - Lib was built Release with flags: -std=c++17 -O3 -march=native -mtune=native
$ # Benchmark hardware info
$ # -----------------------
$ # CPU: Intel(R) Xeon(R) CPU E5-1650 v4 @ 3.60GHz
$ # Kernel: 4.4.0-131-generic #157-Ubuntu SMP
$ # gcc: gcc (Debian 8.1.0-9) 8.1.0
$
$ # And away we go:
$ cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor
performance
$ g++ -std=c++17 -O3 -march=native -mtune=native -o yamltest yamltest.cpp -lyaml-cpp
$ ./yamltest
Inventory has 10000 items    
After 100 saveAsFile() iterations, the average execution time
per iteration was 0.0521697 seconds.
Michael Goldshteyn
  • 71,784
  • 24
  • 131
  • 181
user1593858
  • 649
  • 7
  • 12
  • Tried your code and got different results (little over 5 seconds to write). Then I though to run with release mode (using Visual Studio) and got much better results taking 0.15 seconds to write and the spike of memory only goes up to ~36MB instead of 108MB (still higher than I would think but better). I thought that when writing code you should use debug mode however if this is going to be the results, not sure how usefully that it going to be (not sure if there is a way to tweak VS default debug mode settings to prevent this level of performance loss but still gain the benefits of debug mode) – ryanzec Jun 17 '18 at 17:45
  • I've never really used Visual Studio or done much Windows development, but building with debug symbols on linux with gcc does not measurably affect performance. It looks like the Visual Studio compiler inserts a ton of instrumentation into the STL for debug builds. Perhaps you can try to disable some of this with the suggestions in this answer and the linked blog post https://stackoverflow.com/a/36809417/1593858. Or this one https://codeyarns.com/2010/09/10/visual-c-iterator-checks-and-slow-stl-code/ – user1593858 Jun 17 '18 at 18:58
  • Thanks for those links, applying those setting changes did help quite a bit, down to a little over 1 seconds now. – ryanzec Jun 17 '18 at 20:13