Почему sqlite хранит другое значение при запуске через valgrind? - PullRequest
1 голос
/ 21 мая 2019

В приложении, которое я пишу, я замечаю, что когда я пытаюсь вставить double в базу данных 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;
}

Я проверил, что проблема возникает при сохранении значения, а не при его получении (то есть база данных фактически содержит различающееся двойное число). Вывод вышеуказанной программы:

[~/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 преобразует строку в удвоенную, но кто-нибудь может объяснить, почему это происходит, только когда работает valgrind?

Во-вторых, я могу избавиться от этой проблемы? В финальной программе точность даже не будет столь важной, но во время разработки было бы хорошо иметь предсказуемый результат. Я работаю с некоторыми большими двоичными файлами, и при тестировании самый простой способ проверить правильность работы программы - сравнить сгенерированный выходной файл (который включает в себя базу данных) с известным хорошим. Есть ли способ заставить sqlite просто вставить 8 байтов, которые я хочу?

Спасибо!

1 Ответ

3 голосов
/ 21 мая 2019

За документация :

Затем ваша программа запускается на синтетическом процессоре, предоставляемом ядром Valgrind.

Итак, на бумаге существует множество возможностей для изменения поведения вашей программы при запуске под Valgrind. Конечно, на практике мы надеемся, что этого не произойдет, потому что наши программы должны быть не только переносимыми, но и если Valgrind изменяет их поведение, то это не совсем тестирование того, что мы хотели протестировать даже на той же платформе.

Однако ваши ожидания уже непереносимы, и разработчики используют это в качестве защиты в разделе Ограничения :

Начиная с версии 3.0.0, Valgrind имеет следующие ограничения в реализации x86 / AMD64 с плавающей запятой относительно IEEE754.

Точность: Нет поддержки 80-битной арифметики. Внутренне Valgrind представляет все такие «длинные двойные» числа в 64 битах, и поэтому могут быть некоторые различия в результатах. Вопрос о том, является ли это критическим, еще неизвестно. Обратите внимание, что инструкции x86 / amd64 fldt / fstpt (чтение / запись 80-битных чисел) корректно симулируются с использованием преобразований в / из 64-битных форматов, поэтому образы 80-битных чисел в памяти выглядят корректно, если кто-то захочет их увидеть.

Из многих регрессионных тестов FP складывается впечатление, что различия в точности незначительны. Вообще говоря, если программа использует 80-битную точность, могут возникнуть трудности при переносе ее на платформы, отличные от x86 / amd64, которые поддерживают только 64-битную точность FP. Даже на x86 / amd64 программа может получить разные результаты в зависимости от от того, скомпилировано ли оно для использования инструкций SSE2 (только для 64-разрядных) или команд x87 (для 80-разрядных). Чистый эффект заключается в том, что программы FP ведут себя так, как если бы они выполнялись на машине с 64-разрядными плавающими IEEE, например, PowerPC. В amd64 FP арифметика по умолчанию выполняется в SSE2, поэтому amd64 выглядит более как PowerPC, чем x86 с точки зрения FP, и заметных различий в точности намного меньше, чем с x86.

и

Начиная с версии 3.0.0, Valgrind имеет следующие ограничения в реализации арифметики x86 / AMD64 SSE2 FP относительно IEEE754.

По сути то же самое: никаких исключений и ограниченное соблюдение режима округления. Кроме того, SSE2 имеет контрольные биты, которые заставляют его обрабатывать денормализованные числа как ноль (DAZ) и связанное действие, сбрасывать денормализацию в ноль (FTZ). Оба эти фактора делают арифметику SSE2 менее точной, чем требуется IEEE. Valgrind обнаруживает, игнорирует и может предупреждать о попытках включить любой из режимов.

и др.

Я рекомендую использовать Valgrind для поиска ошибок низкого уровня (утечки памяти и т. П.), А не для проведения какого-либо функционального тестирования.

В качестве альтернативы, если у вас есть восемь конкретных байтов, которые вы хотите поместить в базу данных, просто сделайте это, а не проходите туда-обратно с плавающей запятой.

...