提高驱动程序的时间效率

时间:2019-09-25 14:22:12

标签: c++ linked-list processing-efficiency

很抱歉,标题含糊。本质上,我正在尝试批准C ++驱动程序的时间(和总体)效率:

  1. 使用ifstream逐行读取文件

  2. 对我的程序至关重要的是,行要分开处理,因此我目前有4个单独的调用getline。

  3. 该程序使用string-stream将字符串行读取为整数向量。

  4. 最后,它将向量转换为整数的链接列表。有没有一种方法或函数可以将文件中的整数直接读取到ll个整数中?

以下是驱动程序代码:

@Autowired
private RestTemplate myRestTemplate;

这是一个小的示例输入:

int main(int argc, char *argv[])
{
    ifstream infile(argv[1]);

    vector<int> vals_add;
    vector<int> vals_remove;

    //Driver Code
    if(infile.is_open()){

        string line;
        int n;
        getline(infile, line);
        istringstream iss (line);


        getline(infile, line);
        istringstream iss2 (line);
        while (iss2 >> n){
            vals_add.push_back(n);
        }

        getline(infile, line);
        istringstream iss3 (line);

        getline(infile, line);
        istringstream iss4 (line);
        while (iss4 >> n){
            vals_remove.push_back(n);
        }


        int array_add[vals_add.size()];
        copy(vals_add.begin(), vals_add.end(), array_add);


        int array_remove[vals_remove.size()];
        copy(vals_remove.begin(), vals_remove.end(), array_remove);



        Node *ptr = CnvrtVectoList(array_add, sizeof(array_add)/sizeof(int));
        print(ptr);
        cout << "\n";

        for(int i = 0; i < vals_remove.size(); i++){
           deleteNode(&ptr, vals_remove[i]);
        }


        print(ptr);
        cout << "\n";

    }

第2行和第4行必须作为单独的列表进行处理,而第1行和第3行是列表的大小(它们必须动态分配内存,因此大小必须与输入保持一致)。

2 个答案:

答案 0 :(得分:1)

有很多可以改进的地方。

首先,删除不必要的代码:您未使用ississ3。接下来,您的array_addarray_remove似乎是多余的。直接使用向量。

如果您大致了解平均可以读取多少个值,请在向量中保留空间以避免重复调整大小和复制(实际上,您似乎在输入中包含了这些数字; 使用而不是将其丢弃!)。您还可以将while的阅读循环替换为std::copystd::istream_iterator

您还没有展示CnvrtVectoList的实现方式,但是通常链表由于缺乏本地性而使用起来并不是特别有效:它们会在整个堆中抛出数据。连续的容器(=向量)几乎总是更有效,即使您需要删除中间的元素也是如此。尝试改用向量,并仔细计时性能。

最后,您可以排序这些值吗?如果是这样,则可以使用对std::lower_bound的迭代调用或对std::set_difference的单个调用来更有效地实现值的删除。

如果(并且仅当!)开销实际上是从文件中读取数字,因此请重新构造IO代码,不要单独读取行(这样可以避免很多冗余分配)。相反,请直接扫描输入文件(可以选择使用缓冲区或内存映射),并手动跟踪遇到的换行符。然后,您可以使用strtod系列函数来扫描输入读取缓冲区中的数字。

或者,如果可以假定输入正确,则可以避免使用文件中提供的信息来读取单独的行:

int add_num;
infile >> add_num;
std::copy_n(std::istream_iterator<int>(infile), std::inserter(your_list, std::end(your_list));

int del_num;
infile >> del_num;
std::vector<int> to_delete(del_num);
std::copy_n(std::istream_iterator<int>(infile), del_num, to_delete.begin());
for (auto const n : del_num) {
    deleteNode(&ptr, n);
}

答案 1 :(得分:0)

首先:为什么要使用一些自定义列表数据结构?它很半熟,即不支持分配器,因此很难更好地适应性能。只需将std::list用于双向链接列表,或将std::forward_list用于单链接列表。容易。

您似乎暗示了几个要求:

  1. T类型的值(例如:int)将存储在链接列表中-std::list<T>std::forward_list<T>不是 Node的原始列表)。

  2. 不应不必要地复制数据-即,不应重新分配内存块。

  3. 解析应该是可并行的,尽管这仅在I / O不会占用CPU时间的快速数据源上才有意义。

当时的想法是:

  1. 使用自定义分配器在可存储多个列表节点的连续段中划分内存。

  2. 将整个文件解析为使用上述分配器的链表。该列表将按需分配内存段。每条换行符都会开始一个新列表。

  3. 返回第二和第四列表(即第二和第四行中的元素列表)。

值得注意的是,包含元素计数的行是不必要的。当然,该数据可以传递给分配器以预分配足够的内存段,但这不允许并行化,因为并行解析器不知道元素计数在哪里-只有在对并行解析的数据进行协调后才能找到这些元素。是的,只需进行少量修改,就可以完全并行化该解析。那太酷了!

让我们从简单到最小开始:解析文件以生成两个列表。以下示例在数据集的内部生成的文本视图上使用了std::istringstream,但是parse当然也可以通过std::ifstream传递。

// https://github.com/KubaO/stackoverflown/tree/master/questions/linked-list-allocator-58100610
#include <forward_list>
#include <iostream>
#include <sstream>
#include <vector>

using element_type = int;

template <typename allocator> using list_type = std::forward_list<element_type, allocator>;

template <typename allocator>
std::vector<list_type<allocator>> parse(std::istream &in, allocator alloc)
{
   using list_t = list_type<allocator>;
   std::vector<list_t> lists;
   element_type el;
   list_t *list = {};
   do {
      in >> el;
      if (in.good()) {
         if (!list) list = &lists.emplace_back(alloc);
         list->push_front(std::move(el));
      }
      while (in.good()) {
         int c = in.get();
         if (!isspace(c)) {
            in.unget();
            break;
         }
         else if (c=='\n') list = {};
      }
   } while (in.good() && !in.eof());
   for (auto &list : lists) list.reverse();
   return lists;
}

然后进行测试:

const std::vector<std::vector<element_type>> test_data = {
   {6, 18, 5, 20, 48, 2, 97},
   {3, 6, 9, 12, 28, 5, 7, 10}
};

template <typename allocator = std::allocator<element_type>>
void test(const std::string &str, allocator alloc = {})
{
   std::istringstream input{str};
   auto lists = parse(input, alloc);
   assert(lists.size() == 4);
   lists.erase(lists.begin()+2); // remove the 3rd list
   lists.erase(lists.begin()+0); // remove the 1st list
   for (int i = 0; i < test_data.size(); i++)
      assert(std::equal(test_data[i].begin(), test_data[i].end(), lists[i].begin()));
}

std::string generate_input()
{
   std::stringstream s;
   for (auto &data : test_data) {
      s << data.size() << "\n";
      for (const element_type &el : data) s << el << " ";
      s << "\n";
   }
   return s.str();
}

现在,让我们来看一个自定义分配器:

class segment_allocator_base
{
protected:
   static constexpr size_t segment_size = 128;
   using segment = std::vector<char>;
   struct free_node {
      free_node *next;
      free_node() = delete;
      free_node(const free_node &) = delete;
      free_node &operator=(const free_node &) = delete;
      free_node *stepped_by(size_t element_size, int n) const {
         auto *p = const_cast<free_node*>(this);
         return reinterpret_cast<free_node*>(reinterpret_cast<char*>(p) + (n * element_size));
      }
   };
   struct segment_store {
      size_t element_size;
      free_node *free = {};
      explicit segment_store(size_t element_size) : element_size(element_size) {}
      std::forward_list<segment> segments;
   };
   template <typename T> static constexpr size_t size_for() {
      constexpr size_t T_size = sizeof(T);
      constexpr size_t element_align = std::max(alignof(free_node), alignof(T));
      constexpr auto padding = T_size % element_align;
      return T_size + padding;
   }
   struct pimpl {
      std::vector<segment_store> stores;
      template <typename T> segment_store &store_for() {
         constexpr size_t element_size = size_for<T>();
         for (auto &s : stores)
            if (s.element_size == element_size) return s;
         return stores.emplace_back(element_size);
      }
   };
   std::shared_ptr<pimpl> dp{new pimpl};
};

template<typename T>
class segment_allocator : public segment_allocator_base
{
   segment_store *d = {};
   static constexpr size_t element_size = size_for<T>();
   static free_node *advanced(free_node *p, int n) { return p->stepped_by(element_size, n); }
   static free_node *&advance(free_node *&p, int n) { return (p = advanced(p, n)); }
   void mark_free(free_node *free_start, size_t n)
   {
      auto *p = free_start;
      for (; n; n--) p = (p->next = advanced(p, 1));
      advanced(p, -1)->next = d->free;
      d->free = free_start;
   }
public:
   using value_type = T;
   using pointer = T*;
   template <typename U> struct rebind {
      using other = segment_allocator<U>;
   };
   segment_allocator() : d(&dp->store_for<T>()) {}
   segment_allocator(segment_allocator &&o) = default;
   segment_allocator(const segment_allocator &o) = default;
   segment_allocator &operator=(const segment_allocator &o) {
      dp = o.dp;
      d = o.d;
      return *this;
   }
   template <typename U> segment_allocator(const segment_allocator<U> &o) :
      segment_allocator_base(o), d(&dp->store_for<T>()) {}
   pointer allocate(const size_t n) {
      if (n == 0) return {};
      if (d->free) {
         // look for a sufficiently long contiguous region
         auto **base_ref = &d->free;
         auto *base = *base_ref;
         do {
            auto *p = base;
            for (auto need = n; need; need--) {
               auto *const prev = p;
               auto *const next = prev->next;
               advance(p, 1);
               if (need > 1 && next != p) {
                  base_ref = &(prev->next);
                  base = next;
                  break;
               } else if (need == 1) {
                  *base_ref = next; // remove this region from the free list
                  return reinterpret_cast<pointer>(base);
               }
            }
         } while (base);
      }
      // generate a new segment, guaranteed to contain enough space
      size_t count = std::max(n, segment_size);
      auto &segment = d->segments.emplace_front(count);
      auto *const start = reinterpret_cast<free_node*>(segment.data());
      if (count > n)
         mark_free(advanced(start, n), count - n);
      else
         d->free = nullptr;
      return reinterpret_cast<pointer>(start);
   }
   void deallocate(pointer ptr, std::size_t n) {
      mark_free(reinterpret_cast<free_node*>(ptr), n);
   }

   using propagate_on_container_copy_assignment = std::true_type;
   using propagate_on_container_move_assignment = std::true_type;
};

对于我们得到的少量测试数据,分配器只会分配一个段……一次!

要测试:

int main()
{  
   auto test_input_str = generate_input();
   std::cout << test_input_str << std::endl;
   test(test_input_str);
   test<segment_allocator<element_type>>(test_input_str);
   return 0;
}

并行化将利用上面的分配器,启动多个线程,并且在每次调用自己的分配器上的parse时,每个解析器都从文件中的不同点开始。解析完成后,分配器将必须合并其段列表,以便它们比较相等。此时,可以使用常规方法将链接列表组合在一起。除了线程启动开销之外,并行化的开销可以忽略不计,并且不存在涉及将数据进行并行后组合的数据复制。但是我把这个练习留给读者。