2 Stimmen

Wie lassen sich Probleme mit Zeigeraliasen lösen?

Die unvorsichtige Verwendung von Vorlagen kann zu einer Aufblähung führen. Eine Möglichkeit, diese Aufblähung zu vermeiden, ist eine dünne, typsichere Vorlage, die nicht-typsicheren Nicht-Vorlagen-Code umhüllt. Dazu muss der Wrapper dem Nicht-Template-Code eine Möglichkeit bieten, auf Dinge zuzugreifen, von denen er nichts weiß.

In einer Datenstruktur definiert die Verschalung beispielsweise die Knotenstrukturen. Der unsichere Code muss die Knoten lesen und schreiben, aber er muss dies indirekt tun, über eine Art Schnittstelle, die vom Wrapper festgelegt wird.

Eine Möglichkeit, diese Schnittstelle zu implementieren, besteht darin, eine (durch den unsicheren Code definierte) Struktur mit Details wie Funktionszeigern und Konstanten zu füllen, die vom Wrapper bestimmt werden. Eine relevante Art von Konstanten ist der Offset (innerhalb einer Struktur) eines bestimmten Feldes. Der unsichere Code kann diesen Offset (und etwas Zeigerarithmetik) verwenden, um direkt auf dieses Feld zuzugreifen.

Dies ist jedoch problematisch - da Optimierer immer aggressiver werden, kann dies zu Zeiger-Alias-Problemen führen. Dies ist insbesondere dann der Fall, wenn Knoten der Bibliothek entkommen können. Zum Beispiel können Knoten aus einem Binärbaum extrahiert und neu verknüpft werden, um eine verknüpfte Liste zu bilden. Ein weiteres, ärgerliches Beispiel tritt bei Unit-Tests auf.

Ich habe derzeit eine Containerbibliothek, die nach diesem Schema geschrieben wurde, und sie verursacht derzeit keines dieser Probleme - aber das wird sie in Kürze. Der Grund, warum sie diese Probleme vermeidet, ist, dass alle Unit-Tests auf Container angewendet werden (nicht auf den zugrundeliegenden Code) und dass die Knoten niemals aus den Containern entkommen. Das heißt, auf die Knoten wird immer auf die gleiche Weise zugegriffen, so dass das Problem der Zeiger-Alias-Optimierung nie auftritt.

Leider muss ich in Kürze zulassen, dass Knoten aus den Containern extrahiert werden, und ich werde wahrscheinlich auch Unit-Tests für den zugrunde liegenden unsicheren Code benötigen.

Anstatt sich mit dieser speziellen Bibliothek zu beschäftigen, habe ich hier einen viel einfacheren Auszug aus einer alten Binärbaum-Bibliothek, die unter denselben Problemen leidet. In VC++9 funktioniert es einfach. Mit MinGW GCC 4.4.0 funktioniert ein Debug-Build, aber ein Release-Build schlägt fehl. Das Problem ist eine Mischung aus Inlining und dem Versagen des Optimierers, Zeiger-Aliasse zu erkennen.

Nur um das klarzustellen - ich will hier nicht "WTF - GOTO!!!" oder ähnliches hören. Das Problem ist die Lösung des Optimierungs-/Zeigerproblems. Obwohl, wenn Sie einen Weg finden können, zu schreiben Tree_To_List die richtig strukturiert ist und keine versteckten/verschleierten Gotos verwendet, um dies zu erreichen, bin ich interessiert.

Außerdem fehlt eine Abstraktionsebene auf der Grundlage von Vorlagen (die Vorlage c_Bin_Tree_Tool erledigt nicht die ganze Arbeit - c_Tool erledigt das Wrapping, aber auf eine Art und Weise, die nur für die jeweilige Verwendung gilt, und nicht in wiederverwendbarer Form. Das ist nur ein Nebeneffekt der Extraktion des relevanten Codes.

Mit diesem Code wird ein unausgewogener Binärbaum erstellt, indem ein Knoten nach dem anderen eingefügt wird, und dieser Baum dann ausgeglichen. Der Ausgleich erfolgt durch Umwandlung des Baums in eine Liste (was er in gewisser Weise bereits ist) und anschließende Rückumwandlung der Liste in einen Baum. Der Baum wird sowohl vor als auch nach dem Ausgleichen nach stdio ausgegeben.

bintree.h ...

inline void* Ptr_Add  (void* p1, std::ptrdiff_t p2)  {  return (void*) (((std::ptrdiff_t) p1) + p2);  }

struct c_Bin_Tree_Closure
{
  typedef int (*c_Node_Comp) (void* p_Node1, void* p_Node2);

  c_Node_Comp    m_Node_Comp;
  std::ptrdiff_t m_Left, m_Right;
};

class c_BT_Policy_Closure
{
  private:
    const c_Bin_Tree_Closure* m_Closure;

  protected:
    void** Left_Of  (void* p)  {  return ((void**) Ptr_Add (p, m_Closure->m_Left ));  }
    void** Right_Of (void* p)  {  return ((void**) Ptr_Add (p, m_Closure->m_Right));  }

    int Compare_Node (void* p_Node1, void* p_Node2) const
    {
      return m_Closure->m_Node_Comp (p_Node1, p_Node2);
    }

  public:
    c_BT_Policy_Closure ()
    {
      m_Closure = 0;
    }

    void Set_Closure (const c_Bin_Tree_Closure& p_Closure)
    {
      m_Closure = &p_Closure;
    }
};

template<class tc_Policy>
class c_Bin_Tree_Tool : public tc_Policy
{
  public:
    c_Bin_Tree_Tool ()
    {
    }

    void *List_To_Tree (size_t p_Size, void* &p_List);
    void  Tree_To_List (void* p_Root, void* &p_First, void* &p_Last, size_t &p_Size);
    void  Balance      (void* &p);
    void  Insert       (void* &p_Tree, void* p_Node);
};

template<class tc_Policy>
void* c_Bin_Tree_Tool<tc_Policy>::List_To_Tree (size_t p_Size, void* &p_List)
{
  if (p_Size == 0)  return 0;

  size_t l_Size = p_Size / 2;
  void*  l_Ptr  = List_To_Tree (l_Size, p_List);

  void* l_This = p_List;
  p_List = *tc_Policy::Right_Of (l_This);

  *tc_Policy::Left_Of (l_This) = l_Ptr;

  l_Size = p_Size - (l_Size + 1);
  *tc_Policy::Right_Of (l_This) = List_To_Tree (l_Size, p_List);

  return l_This;
}

template<class tc_Policy>
void c_Bin_Tree_Tool<tc_Policy>::Tree_To_List (void* p_Root, void* &p_First, void* &p_Last, size_t &p_Size)
{
  //  Use left links as prev links and right links as next links

  void*  l_Start    = 0;         //  first-item-in-list pointer
  void*  l_Prev     = 0;         //  previous node in list
  void** l_Prev_Ptr = &l_Start;  //  points to the next (ie right) pointer for the next node.
  void*  l_Pos      = p_Root;
  void*  l_Next;
  void*  l_Parent   = 0;
  size_t l_Count    = 0;

  p_Last = 0;

  TOP_OF_LOOP:
    l_Next = *tc_Policy::Left_Of (l_Pos);

    if (l_Next != 0)
    {
      *tc_Policy::Left_Of (l_Pos) = l_Parent;  //  So we can find our way back up the tree
      l_Parent = l_Pos;
      l_Pos    = l_Next;
      goto TOP_OF_LOOP;
    }

  AFTER_STEP_PARENT:
    l_Next = *tc_Policy::Right_Of (l_Pos);

    *tc_Policy::Left_Of (l_Pos) = l_Prev;

    p_Last      = l_Pos;
    l_Prev      = l_Pos;
    *l_Prev_Ptr = l_Pos;
    l_Prev_Ptr  = tc_Policy::Right_Of (l_Pos);
    l_Count++;

    if (l_Next != 0)
    {
      l_Pos = l_Next;
      goto TOP_OF_LOOP;
    }

    if (l_Parent != 0)
    {
      l_Pos    = l_Parent;
      l_Parent = *tc_Policy::Left_Of (l_Pos);
      goto AFTER_STEP_PARENT;
    }

  *l_Prev_Ptr = 0;
  p_First     = l_Start;
  p_Size      = l_Count;
}

template<class tc_Policy>
void c_Bin_Tree_Tool<tc_Policy>::Balance (void* &p)
{
  void   *l_First, *l_Last;
  size_t l_Count;

  Tree_To_List (p, l_First, l_Last, l_Count);
  p = List_To_Tree (l_Count, l_First);
}

template<class tc_Policy>
void c_Bin_Tree_Tool<tc_Policy>::Insert (void* &p_Tree, void* p_Node)
{
  void** l_Tree = &p_Tree;

  while (*l_Tree != 0)
  {
    int l_Compare = tc_Policy::Compare_Node (*l_Tree, p_Node);
    l_Tree = ((l_Compare < 0) ? tc_Policy::Right_Of (*l_Tree) : tc_Policy::Left_Of (*l_Tree));
  }

  *l_Tree = p_Node;
  *tc_Policy::Right_Of (p_Node) = 0;
  *tc_Policy::Left_Of  (p_Node) = 0;
};

test_bintree.cpp ...

#include <iostream>

#include "bintree.h"

struct c_Node
{
  c_Node *m_Left, *m_Right;
  int    m_Data;
};

c_Node g_Node0001 = { 0, 0,  1 };  c_Node g_Node0002 = { 0, 0,  2 };
c_Node g_Node0003 = { 0, 0,  3 };  c_Node g_Node0004 = { 0, 0,  4 };
c_Node g_Node0005 = { 0, 0,  5 };  c_Node g_Node0006 = { 0, 0,  6 };
c_Node g_Node0007 = { 0, 0,  7 };  c_Node g_Node0008 = { 0, 0,  8 };
c_Node g_Node0009 = { 0, 0,  9 };  c_Node g_Node0010 = { 0, 0, 10 };

int Node_Compare (void* p1, void* p2)
{
  return (((c_Node*) p1)->m_Data - ((c_Node*) p2)->m_Data);
}

c_Bin_Tree_Closure  g_Closure =
{
  (c_Bin_Tree_Closure::c_Node_Comp) Node_Compare,
  offsetof (c_Node, m_Left ),  offsetof (c_Node, m_Right)
};

class c_Tool : public c_Bin_Tree_Tool<c_BT_Policy_Closure>
{
  protected:
    typedef c_Bin_Tree_Tool<c_BT_Policy_Closure> c_Base;
  public:
    c_Tool ()  {  Set_Closure (g_Closure);  }
    void Insert  (c_Node* &p_Tree, c_Node* p_Node)  {  c_Base::Insert ((void*&) p_Tree, p_Node);  }
    void Balance (c_Node* &p)  {  c_Base::Balance ((void*&) p);  }
};

void BT_Dump (size_t p_Depth, c_Node* p_Node)
{
  if (p_Node != 0)
  {
    BT_Dump (p_Depth + 1, p_Node->m_Left);
    for (size_t i = 0; i < p_Depth; i++)  std::cout << " .";
    std::cout << " " << p_Node->m_Data << std::endl;
    BT_Dump (p_Depth + 1, p_Node->m_Right);
  }
}

int main (int argc, char* argv[])
{
  c_Tool  l_Tool;
  c_Node *l_Root = 0;

  l_Tool.Insert (l_Root, &g_Node0001);
  l_Tool.Insert (l_Root, &g_Node0002);
  l_Tool.Insert (l_Root, &g_Node0003);
  l_Tool.Insert (l_Root, &g_Node0004);
  l_Tool.Insert (l_Root, &g_Node0005);
  l_Tool.Insert (l_Root, &g_Node0006);
  l_Tool.Insert (l_Root, &g_Node0007);
  l_Tool.Insert (l_Root, &g_Node0008);
  l_Tool.Insert (l_Root, &g_Node0009);
  l_Tool.Insert (l_Root, &g_Node0010);

  BT_Dump (0, l_Root);  std::cout << "----------" << std::endl;

  l_Tool.Balance (l_Root);
  BT_Dump (0, l_Root);

  return 0;
}

Die erwarteten Ergebnisse sind...

 1
 . 2
 . . 3
 . . . 4
 . . . . 5
 . . . . . 6
 . . . . . . 7
 . . . . . . . 8
 . . . . . . . . 9
 . . . . . . . . . 10
----------
 . . . 1
 . . 2
 . 3
 . . . 4
 . . 5
 6
 . . . 7
 . . 8
 . 9
 . . 10

Was tatsächlich passiert (MinGW GCC 4.4.0, optimierter Release-Build)...

 1
 . 2
 . . 3
 . . . 4
 . . . . 5
 . . . . . 6
 . . . . . . 7
 . . . . . . . 8
 . . . . . . . . 9
 . . . . . . . . . 10
----------
 1

Soweit ich das beurteilen kann, läuft die Operation Balance korrekt, aber die Funktion BT_Dump kann nicht alle Änderungen an der m_Left y m_Right Felder.

EDIT Das ist falsch - warum würde ich sonst Knoten 1 als neue Wurzel sehen. Ich schätze, das passiert, wenn man sich zu sehr auf die Erinnerung an eine Untersuchung verlässt, die vor Monaten durchgeführt wurde.

EDIT Eigentlich ist Node 1 als Root ein Problem, aber da es die alte Root war - naja, am besten ignoriert man dieses Was-ist-das-Problem-Zeug einfach und stellt seine eigenen Theorien auf ;-)

Der Code weist eine Reihe von Problemen auf, die nicht durch Standards definiert sind. Ich denke, das größte Problem ist, dass die Links in der Knotenstruktur c_Node* sind, aber da der unsichere Code nichts über c_Node weiß, greift er auf sie (über Zeigerarithmetik) als void* zu.

Eine Lösung wäre, dass der unsichere Code alle Lese- und Schreibvorgänge über Getter- und Setter-Funktionszeiger durchführt, wodurch die gesamte Zeigerarithmetik vermieden und sichergestellt wird, dass alle Zugriffe auf c_Node-Instanzen über c_Node*-Zeiger erfolgen. Noch besser - die Schnittstelle wird zu einer Klasse mit Getter/Setter-Methoden usw. In der kompletten Binärbaum-Bibliothek habe ich alternative Policy-Klassen, die dies tun, obwohl, um ehrlich zu sein, meine wirkliche Lösung, als das Problem auftauchte, war, den ganzen Code in einen "Junk"-Ordner zu werfen, auf der Basis, dass ich ihn selten benutze und wahrscheinlich sowieso aufdringliche Boost-Listen verwenden sollte.

Damit bleibt aber immer noch die andere, viel komplexere und viel genutzte Containerbibliothek übrig, die nicht verschwinden wird. Ich denke, ich bin nur zu tun haben, die sehr schmerzhaft Refactoring, um loszuwerden, die offsetof und Zeiger Arithmetik Zeug, aber...

Was genau sind die C++-Regeln - wann genau darf der Compiler einen möglichen Zeiger-Alias nicht erkennen? Und könnte der obige Binärbaumcode so umgeschrieben werden, dass er immer noch Zeigerarithmetik verwendet, immer noch erlaubt, dass auf die Knoten sowohl innerhalb als auch außerhalb der Bibliothek zugegriffen/geändert wird, und dennoch sicher vor diesem Optimierungsproblem ist?

3voto

Gunther Piez Punkte 28746

Haben Sie die Warnungen ausgeschaltet? Sie sollten einige "dereferencing type punned pointers violates strict aliasing" bekommen haben, denn das ist genau das, was Sie bei (void**) Ptr_Add(...) tun.

Der Compiler kann davon ausgehen, dass Zeiger auf verschiedene Typen keine Alias-Zeiger sind (mit einigen wenigen Ausnahmen), und wird optimierten Code erzeugen, der die Ziele von Zeigern in Registern zwischenspeichert. Um dies zu vermeiden, müssen Sie Unions verwenden, um zwischen verschiedenen Zeigertypen zu konvertieren. Ich zitiere aus http://gcc.gnu.org/onlinedocs/gcc/Optimize-Options.html#Optimize-Options :

Insbesondere wird davon ausgegangen, dass sich ein Objekt eines Typs niemals an derselben Adresse wie ein Objekt eines anderen Typs befindet, es sei denn, die Typen sind nahezu identisch. Zum Beispiel kann ein unsigned int ein int alias sein, aber nicht ein void* oder ein double. Ein Zeichentyp kann jeden anderen Typ als Alias verwenden.

Achten Sie besonders auf Code wie diesen:

     union a_union {
        int i;
        double d;
      };

     int f() {
        union a_union t;
        t.d = 3.0;
        return t.i;
      }

Die Praxis, von einem anderen Gewerkschaftsmitglied zu lesen als dem, in das zuletzt geschrieben wurde (genannt "type-punning"), ist üblich. Sogar mit -fstrict-aliasing ist type-punning erlaubt, sofern auf den Speicher über den Union-Typ zugegriffen wird. Der obige Code wird also wie erwartet funktionieren. Siehe Structures unions enumerations and bit-fields implementation. Dieser Code jedoch möglicherweise nicht:

     int f() {
        union a_union t;
        int* ip;
        t.d = 3.0;
        ip = &t.i;
        return *ip;
      }

Auch der Zugriff über die Adresse, das Casting des resultierenden Zeigers und die Dereferenzierung des Ergebnisses hat ein undefiniertes Verhalten, selbst wenn das Casting einen Union-Typ verwendet, z. B.:

     int f() {
        double d = 3.0;
        return ((union a_union *) &d)->i;
      }

Die Option -fstrict-aliasing ist in den Stufen -O2, -O3, -Os aktiviert.

In Ihrem Fall könnten Sie etwas verwenden wie

union {
    void** ret_ptr;
    ptrdiff_t in_ptr;
}

aber der Code in ptr_add sieht einfach furchtbar aus ;-)

Oder deaktivieren Sie einfach diese spezielle Optimierung mit "fno-strict-aliasing". Reparieren Sie aber besser Ihren Code ;-)

0voto

Puppy Punkte 141483

Die unvorsichtige Verwendung von Vorlagen KANN zu einer Aufblähung führen. Aber du verstehst den Punkt hier völlig falsch.

  • Schablonen verursachen Blähungen, wenn sie verwendet werden unvorsichtig und nicht sorgfältig verwendet werden.
  • Die Anzahl der Laufzeitfehler die durch Vorlagen vermieden werden, ist enorm.
  • Die Geschwindigkeit von Schablonencode ist weit größer als nicht-templated Code.
  • Die Größe der ausführbaren Datei ist absolut unbedeutend, es sei denn, sie läuft auf einem eingebetteten System.
  • Die STL bietet einen Map-Container (einen binären Suchbaum) für Ihre Verwendung.

Sie haben die Sache einfach nicht richtig durchdacht. Die Vorteile, die Vorlagen bieten, überwiegen bei weitem die paar KB an ausführbarer Größe.

Es ist auch erwähnenswert, dass der Code wie erwartet unter Visual Studio 2010 funktioniert.

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X