在valgrind中运行时,sqlite为什么会存储不同的值?

时间:2019-05-21 13:41:53

标签: c++ sqlite valgrind

在我正在编写的应用程序中,我注意到当我尝试将双精度型插入sqlite数据库中时,实际上会存储(略有不同)的值,但仅当应用程序通过valgrind运行时才存储。直接调用程序(无需重新编译)时,所有双打均按预期存储。

此代码重现了问题。为简便起见,已删除了一些安全检查等。

#include <sqlite3.h>
#include <iostream>
#include <vector>
#include <any>
#include <memory>
#include <cstring>
#include <iomanip>

class SqliteDB
{
  sqlite3 *d_db;
  bool d_ok;
 public:
  inline SqliteDB(std::string const &name);
  inline ~SqliteDB();
  inline void exec(std::string const &q, std::vector<std::vector<std::pair<std::string, std::any>>> *results);
};

inline SqliteDB::SqliteDB(std::string const &name)
  :
  d_db(nullptr),
  d_ok(false)
{
  d_ok = (sqlite3_open(name.c_str(), &d_db) == 0);
}

inline SqliteDB::~SqliteDB()
{
  if (d_ok)
    sqlite3_close(d_db);
}

inline void SqliteDB::exec(std::string const &q, std::vector<std::vector<std::pair<std::string, std::any>>> *results)
{
  sqlite3_stmt *stmt;
  if (sqlite3_prepare_v2(d_db, q.c_str(), -1, &stmt, nullptr) != SQLITE_OK)
  {
    std::cout << "SQL Error: " << sqlite3_errmsg(d_db) << std::endl;
    return;
  }
  int rc;
  results->clear();
  while ((rc = sqlite3_step(stmt)) == SQLITE_ROW)
  {
    results->resize(results->size() + 1);
    for (int i = 0; i < sqlite3_column_count(stmt); ++i)
    {
      if (sqlite3_column_type(stmt, i) == SQLITE_INTEGER)
      {
        results->back().emplace_back(std::make_pair(sqlite3_column_name(stmt, i), sqlite3_column_int64(stmt, i)));
      }
      else if (sqlite3_column_type(stmt, i) == SQLITE_FLOAT)
      {
        results->back().emplace_back(std::make_pair(sqlite3_column_name(stmt, i), sqlite3_column_double(stmt, i)));
      }
      else if (sqlite3_column_type(stmt, i) == SQLITE_TEXT)
      {
        results->back().emplace_back(std::make_pair(sqlite3_column_name(stmt, i), std::string(reinterpret_cast<char const *>(sqlite3_column_text(stmt, i)))));
      }
      else if (sqlite3_column_type(stmt, i) == SQLITE_BLOB)
      {
        size_t blobsize = sqlite3_column_bytes(stmt, i);
        std::shared_ptr<unsigned char []> blob(new unsigned char[blobsize]);
        std::memcpy(blob.get(), reinterpret_cast<unsigned char const *>(sqlite3_column_blob(stmt, i)), blobsize);
        results->back().emplace_back(std::make_pair(sqlite3_column_name(stmt, i), std::make_pair(blob, blobsize)));
      }
      else if (sqlite3_column_type(stmt, i) == SQLITE_NULL)
      {
        results->back().emplace_back(std::make_pair(sqlite3_column_name(stmt, i), nullptr));
      }
    }
  }
  if (rc != SQLITE_DONE)
    std::cout << "SQL Error: " << sqlite3_errmsg(d_db) << std::endl;
  sqlite3_finalize(stmt);
}

inline std::string toHexString(double d)
{
  unsigned char *data = reinterpret_cast<unsigned char *>(&d);
  std::ostringstream oss;
  oss << "(hex:) ";
  for (uint i = 0; i < sizeof(d); ++i)
    oss << std::hex << std::setfill('0') << std::setw(2)
        << (static_cast<int32_t>(data[i]) & 0xFF)
        << ((i == sizeof(d) - 1) ? "" : " ");
  return oss.str();
}

int main()
{
  SqliteDB db(":memory:");
  std::vector<std::vector<std::pair<std::string, std::any>>> results;

  db.exec("CREATE TABLE part (_id INTEGER PRIMARY KEY, ratio REAL)", &results);

  double d = 1.4814814329147339;
  std::cout << "Inserting into table: " << std::defaultfloat << std::setprecision(17) << d
            << " " << toHexString(d) << std::endl;

  db.exec("INSERT INTO part VALUES (1,1.4814814329147339)", &results);
  db.exec("SELECT ratio FROM part WHERE _id = 1", &results);
  for (uint i = 0; i < results.size(); ++i)
    for (uint j = 0; j < results[i].size(); ++j)
    {
      if (results[i][j].second.type() == typeid(double))
        std::cout << "Retrieved from table: " << std::defaultfloat << std::setprecision(17) << std::any_cast<double>(results[i][j].second)
                  << " " << toHexString(std::any_cast<double>(results[i][j].second)) << std::endl;
    }

  return 0;
}

我已经验证了问题是在存储值时发生的,而不是在获取值时发生的(也就是说,数据库实际上包含不同的double值)。上面程序的输出:

[~/valgrindsqlitedouble] $ g++ -std=c++2a -Wall -Wextra -Wshadow -Wold-style-cast -pedantic -fomit-frame-pointer -O1 -g -lsqlite3 main.cc
[~/valgrindsqlitedouble] $ ./a.out 
Inserting into table: 1.4814814329147339 (hex:) 00 00 00 e0 25 b4 f7 3f
Retrieved from table: 1.4814814329147339 (hex:) 00 00 00 e0 25 b4 f7 3f
[~/valgrindsqlitedouble] $ valgrind ./a.out 
==3340== Memcheck, a memory error detector
==3340== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==3340== Using Valgrind-3.14.0 and LibVEX; rerun with -h for copyright info
==3340== Command: ./a.out
==3340== 
Inserting into table: 1.4814814329147339 (hex:) 00 00 00 e0 25 b4 f7 3f
Retrieved from table: 1.4814814329147341 (hex:) 01 00 00 e0 25 b4 f7 3f
==3340== 
==3340== HEAP SUMMARY:
==3340==     in use at exit: 0 bytes in 0 blocks
==3340==   total heap usage: 299 allocs, 299 frees, 269,972 bytes allocated
==3340== 
==3340== All heap blocks were freed -- no leaks are possible
==3340== 
==3340== For counts of detected and suppressed errors, rerun with: -v
==3340== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)
[~/valgrindsqlitedouble] $

我认为sqlite将字符串转换为double时会发生舍入错误,但是谁能解释为什么只有在valgrind运行时才会发生?

第二,我可以摆脱这个问题吗?在最终程序中,精度甚至不那么重要,但是在开发过程中,具有可预测的输出将是很好的。我正在处理一些大的二进制文件,并且在测试验证程序正常运行的最简单方法是将生成的输出文件(包括数据库)与已知的输出文件进行比较。有什么办法可以让sqlite插入我想要的8个字节?

谢谢!

1 个答案:

答案 0 :(得分:3)

the documentation

  

然后,您的程序将在Valgrind内核提供的合成CPU上运行。

因此在纸上,在Valgrind下运行时,程序的行为有很大的变化范围。当然,实际上,我们希望情况并非如此,因为我们的程序不仅应该是可移植的,而且如果Valgrind更改其行为,那么即使在同一平台上,它也无法真正测试我们想要测试的内容。 / p>

但是,您的期望已经无法移植,开发人员在其 Limitations 部分中将其用作辩护:

  

从3.0.0版开始,Valgrind在相对于IEEE754的x86 / AMD64浮点实现中具有以下限制。

     

精度:不支持80位算术。在内部,Valgrind用64位表示所有这样的“长双精度”数字,因此结果可能会有所不同。这是否很关键还有待观察。请注意,x86 / amd64 fldt / fstpt指令(读/写80位数字)已使用到64位的转换进行了正确模拟,因此,如果有人希望看到,则80位数字的内存图像看起来正确。

     

从许多FP回归测试中观察到的印象是准确性差异不明显。一般来说,如果程序依赖于80位精度,则可能很难将其移植到仅支持64位FP精度的非x86 / amd64平台上。即使在x86 / amd64上,该程序也可能会得到不同的结果。取决于它是被编译为使用SSE2指令(仅64位)还是x87指令(80位)。 最终结果是使FP程序的行为就像在具有64位IEEE浮点数的计算机(例如PowerPC)上运行一样。在amd64上,FPSE算法默认在SSE2上完成,因此amd64看起来从FP的角度来看,它比x86更像PowerPC,并且与x86相比,明显的精度差异要少得多。

  

从3.0.0版开始,相对于IEEE754,Valgrind在其x86 / AMD64 SSE2 FP算术实现中具有以下限制。

     

基本上相同:没有例外,并且对舍入模式的遵守有限。同样,SSE2具有控制位,使它可以将非规范化的数字视为零(DAZ)以及相关的动作,将非规范化的数字刷新为零(FTZ)。 这两种情况都会导致SSE2算法的准确性低于IEEE要求。 Valgrind会检测到,忽略并警告启用任何一种模式的尝试。

我建议使用Valgrind查找低级错误(内存泄漏等),而不是执行任何功能测试。

或者,如果您有八个特定的字节要放入数据库中,只需执行此操作即可,而不是进行浮点往返。