如何减少结构尺寸

时间:2009-09-04 20:46:10

标签: c# .net data-structures

我有一个包含多个结构的类库,每个结构都包含多个值和引用类型。大多数值类型是必需的,一些值类型和所有引用类型都是可选的。所有结构都是XmlSerializable(这是强制性的)。

至于类库是针对移动设备的,我想减少内存占用。我的第一个想法是使用Nullable<T>作为值类型,但这会使每个Nullable<T>的内存大小增加4个字节。我的第二个想法是将所有可选值类型打包到一个单独的结构中,该结构仅在需要任何成员时进行实例化。但这会迫使我在“主要”结构上实施IXmlSerializable

还有其他方法可以“缩小”结构吗?

[编辑]

请原谅这个糟糕的问题。我想我必须澄清一些事情并且更具体:

类库旨在序列化数据信息GPX (GPS Exchange Format)。结构例如是Waypoint或Track。它们具有纬度,经度等必填字段。可选字段为垂直/水平/精度位置稀释,描述和链接。

该库主要针对移动设备,例如PDA。 RAM很短,但可以使用大量非易失性存储器。

只要没有代码示例就无法显示。我想在开始实施之前考虑几个陷阱。

6 个答案:

答案 0 :(得分:8)

这是一种在允许Xml序列化的同时积极降低内存开销的技术 更新:对于1和2个条目,orignal内联链表的想法比带有count构造的标准列表更有效,但对零,一和两种情况使用固定大小的选项更有效。

限制性条款:

  

这是基于你知道你真的需要削减内存,因此   (因为你还没有做任何编码)这可能是一个非常不成熟的事   优化

此设计也基于非常罕见的可选字段。

我使用double作为'占位符',无论哪种格式最佳,都可以表示所使用的精度/单位。

public class WayPoint
{
  // consumes IntPtr.Size fixed cost 
  private IOptional optional = OptionalNone.Default; 

  public double Latitude  { get; set; }
  public double Longitude { get; set; }

  public double Vertical 
  { 
    get { return optional.Get<double>("Vertical") ?? 0.0; }
    set { optional = optional.Set<double>("Vertical", value); } 
  }

  [XmlIgnore] // need this pair for every value type  
  public bool VerticalSpecified 
  { 
    get { return optional.Get<double>("Vertical").HasValue;  } 
  }         
  public void ClearVertical()
  {
    optional = optional.Clear<double>("Vertical");   
  }

  public string Description // setting to null clears it
  { 
    get { return optional.GetRef<string>("Description"); }
    set { optional = optional.SetRef<string>("Description", value); } 
  }

  // Horizontal, Position, DilutionOfPrecision etc.
}

真正繁重的工作在这里完成:

internal interface IOptional
{
  T? Get<T>(string id) where T : struct;
  T GetRef<T>(string id) where T : class;

  IOptional Set<T>(string id, T value);
  IOptional Clear(string id);
}

internal sealed class OptionalNone : IOptional
{
  public static readonly OptionalNone Default = new OptionalNone();

  public T? Get<T>(string id)  where T : struct
  { 
    return null;
  }

  public T GetRef<T>(string id)  where T : class
  {
    return null;
  }

  public IOptional Set<T>(string id, T value)
  {
    if (value == null)
      return Clear(id);
    return new OptionalWithOne<T>(id, value);
  }

  public IOptional Clear(string id)
  {
    return this; // no effect
  }
}

固定大小的写入变得更有趣,没有必要将它们写为结构,因为它们将被装箱以放置在WayPoint类的IOptional字段中。

internal sealed class OptionalWithOne<X> : IOptional
{
  private string id1;
  private X value1;

  public OptionalWithOne(string id, X value)
  {
    this.id1 = id;
    this.value1 = value;
  }

  public T? Get<T>(string id)  where T : struct
  { 
    if (string.Equals(id, this.id1))
      return (T)(object)this.value1;
    return null;
  }

  public T GetRef<T>(string id)  where T : class        
  {
    if (string.Equals(id, this.id1))
      return (T)(object)this.value1;
    return null;
  }

  public IOptional Set<T>(string id, T value)
  {
    if (string.Equals(id, this.id1))
    {
      if (value == null)
        return OptionalNone.Default;
      this.value1 = (X)(object)value;
      return this;
    }
    else
    {
      if (value == null)
        return this;
      return new OptionalWithTwo<X,T>(this.id1, this.value1, id, value);
    }
  }

  public IOptional Clear(string id)
  {
    if (string.Equals(id, this.id1))
      return OptionalNone.Default;
    return this; // no effect
  }
}

然后是两个(你可以根据需要扩展这个想法,但你可以看到代码很快就会变得不愉快。

internal sealed class OptionalWithTwo<X,Y> : IOptional
{
  private string id1;
  private X value1;
  private string id2;
  private Y value2;

  public OptionalWithTwo(
    string id1, X value1,
    string id2, Y value2)
  {
    this.id1 = id1;
    this.value1 = value1;
    this.id2 = id2;
    this.value2 = value2;
  }

  public T? Get<T>(string id)  where T : struct
  { 
    if (string.Equals(id, this.id1))
      return (T)(object)this.value1;
    if (string.Equals(id, this.id2))
      return (T)(object)this.value2;
    return null;
  }

  public T GetRef<T>(string id)  where T : class        
  {
    if (string.Equals(id, this.id1))
      return (T)(object)this.value1;
    if (string.Equals(id, this.id2))
      return (T)(object)this.value2;
    return null;
  }

  public IOptional Set<T>(string id, T value)
  {
    if (string.Equals(id, this.id1))
    {
      if (value == null)
        return Clear(id);
      this.value1 = (X)(object)value;
      return this;
    }
    else if (string.Equals(id, this.id2))
    {
      if (value == null)
        return Clear(id);
      this.value2 = (Y)(object)value;
      return this;
    }
    else
    {
      if (value == null)
        return this;
      return new OptionalWithMany(
        this.id1, this.value1,
        this.id2, this.value2,
        id, value);
    }
  }

  public IOptional Clear(string id)
  {
    if (string.Equals(id, this.id1))
      return new OptionalWithOne<Y>(this.id2, this.value2);
    if (string.Equals(id, this.id2))
      return new OptionalWithOne<X>(this.id1, this.value1);
    return this; // no effect
  }
} 

最终以相对低效的

结束
internal sealed class OptionalWithMany : IOptional
{
  private List<string> ids = new List<string>();
  // this boxes, if you had a restricted set of data types 
  // you could do a per type list and map between them
  // it is assumed that this is sufficiently uncommon that you don't care
  private List<object> values = new List<object>();

  public OptionalWithMany(
    string id1, object value1,
    string id2, object value2,
    string id3, object value3)
  {
    this.ids.Add(id1);
    this.values.Add(value1);
    this.ids.Add(id2);
    this.values.Add(value2);
    this.ids.Add(id3);
    this.values.Add(value3);
  }

  public T? Get<T>(string id)  where T : struct
  { 
    for (int i= 0; i < this.values.Count;i++)
    { 
      if (string.Equals(id, this.ids[i]))
        return (T)this.values[i];
    }
    return null;
  }

  public T GetRef<T>(string id)  where T : class        
  {
    for (int i= 0; i < this.values.Count;i++)
    { 
      if (string.Equals(id, this.ids[i]))
        return (T)this.values[i];
    }
    return null;
  }

  public IOptional Set<T>(string id, T value)
  {
    for (int i= 0; i < this.values.Count;i++)
    { 
      if (string.Equals(id, this.ids[i]))
      {
        if (value == null)
          return Clear(id);           
        this.values[i] = value;
        return this;
      }
    }
    if (value != null)
    {
      this.ids.Add(id);
      this.values.Add(value);
    }  
    return this;  
  }

  public IOptional Clear(string id)
  {
    for (int i= 0; i < this.values.Count;i++)
    { 
      if (string.Equals(id, this.ids[i]))
      {
        this.ids.RemoveAt(i);
        this.values.RemoveAt(i);
        return ShrinkIfNeeded();
      }
    }
    return this; // no effect
  }

  private IOptional ShrinkIfNeeded()
  {
    if (this.ids.Count == 2)
    {
      //return new OptionalWithTwo<X,Y>(
      // this.ids[0], this.values[0],
      //  this.ids[1], this.values[1]);
      return (IOptional)
        typeof(OptionalWithTwo<,>).MakeGenericType(
          // this is a bit risky. 
          // your value types may not use inhertence
          this.values[0].GetType(),
          this.values[1].GetType())
        .GetConstructors().First().Invoke(
          new object[] 
          {
            this.ids[0], this.values[0],
            this.ids[1], this.values[1]
          });
    }
    return this;
  }
}   

OptionalWithMany可以写得比这更好但它给你的想法。 通过限制类型支持,您可以执行全局密钥 - &gt;每种类型'堆'的值映射如下:

internal struct Key
{ 
    public readonly OptionalWithMany;
    public readonly string Id;
    // define equality and hashcode as per usual
}    

然后只需在OptionalToMany中存储当前正在使用的Id列表。缩小会稍微复杂一些(但从类型的角度来看更好,因为你会扫描每个全局'堆',直到找到匹配的条目并使用堆的类型来构造OptionalWithTwo。这将允许属性值中的多态性

无论内部情况如何,其主要好处是WayPoint类的公共表面完全隐藏了这一切。

然后你可以通过属性IXmlSerializable来设置你想要序列化的类,这样就不需要烦人的xxxSpecified属性了。

在我的例子中,我使用字符串作为Id的简单 如果您真的关心大小和速度,您应该将Id更改为枚举。给定打包行为,即使您可以将所有需要的值放入一个字节中,这也不会为您节省太多,但它会为您提供编译时的健全性检查。字符串都是编译时常量,因此占用的空间旁边没有空格(但检查相等性的速度较慢)。

我强烈建议您在之后只做这样的事情,检查是否需要它。好的一面是,这并不限制你的xml序列化,所以你可以把它塑造成你想要的任何格式。此外,'数据包'的公共面可以保持清洁(除了xxxSpecified垃圾)。

如果你想避免xxxSpecified麻烦,你知道你有一些'带外'值,你可以使用以下技巧:

[DefaultValue(double.MaxValue)]
public double Vertical 
{ 
    get { return optional.Get<double>("Vertical") ?? double.MaxValue; }
    set { optional = optional.Set<double>("Vertical", value); } 
}

public void ClearVertical()
{
    optional = optional.ClearValue<double>("Vertical");   
}

但是,您的其余API必须能够检测这些特殊值。一般来说,我会说指定的路线更好。

如果特定的一组属性在某些设备上变得“始终可用”,或者在某些模式下,您应该切换到其中属性为简单类的备用类。由于xml表单相同,这意味着它们可以简单轻松地进行互操作,但在这些情况下内存使用量将会少得多。

如果这些组的数量变大,您甚至可以考虑代码生成场景(甚至在运行时,尽管这会大大增加您的支持负担)

答案 1 :(得分:3)

为了一些重要的乐趣: 应用Flyweight并将所有实例存储在位图中?使用小型存储设备,您不需要4个字节的指针。

[编辑]使用Flyweight,您可以为每个字段设置单独的存储策略。我不建议将字符串值直接存储在位图中,但您可以存储索引。

该类型不存储在位图中,而是存储在唯一对象工厂中。

答案 2 :(得分:2)

可能很高兴知道XmlSerializer不关心您的内部对象布局,它只关心您的公共字段和属性。您可以隐藏属性访问器后面的内部内存优化,而XmlSerializer甚至都不知道。

例如,如果您知道通常只设置了2个引用,但有时候更多,您可以将两个频繁的引用存储为主对象的一部分,并将不常见的引用隐藏在对象[]或ListDictionary中或你自己制作的专业私人课程。但是,请注意每个间接容器对象也包含开销,因为它需要是引用类型。或者当你有8个可空整数作为公共合同的一部分时,你可以在内部使用8个常规整数和一个包含is-this-int-null状态的字节作为其位。

如果你想进一步专业化,也许根据可用数据创建专门的子类,你必须走IXmlSerializable的路线,但通常不需要。

答案 3 :(得分:2)

你可以做几件事:

  • 确保使用特定值的最小类型。例如,如果查看架构,则dgpsStationType的最小值为0,最大值为1023.这可以存储为ushort。尽可能减小这些物品的尺寸。

  • 确保您的字段是4字节对齐的。最终得到的结构大小将是4字节大小的倍数(假设为32位)。类的默认布局包含按顺序存储的项。如果字段未正确打包,编译器将浪费空间,确保您的字段是4字节对齐的。您可以使用StructLayoutAttribute显式指定布局。

错误示例:类中的这些字段占用12个字节。 int必须占用4个连续字节,其他成员必须是4字节对齐。

public class Bad {
   byte a;
   byte b;
   int c;
   ushort u; 
}

更好的示例:类中的这些字段占用8个字节。这些字段有效地打包。

public class Better {
   byte a;
   byte b;
   ushort u;
   int c;
}
  • 缩小对象图的大小。每种引用类型占用8个字节的开销。如果你有一个深度图,那就是很多开销。将您可以使用的所有内容放入对主类中的数据进行操作的函数中。认为更像'C',而不是OOD。

  • 延迟加载一些可选参数仍然是一个好主意,但你应该清楚地画出你的线条。创建1个或可能2个可加载的“可选”值或null。每个集合将强制要求引用类型及其开销。

  • 尽可能使用结构。但要注意值类型语义,它们可能很棘手。

  • 考虑不实施ISerializable。接口方法根据定义是虚拟的。任何具有虚方法的类都包含对vtable的引用(另外4个字节)。而是在外部类中手动实现xml序列化。

答案 4 :(得分:0)

某种二进制序列化通常比XML序列化要好得多。您必须尝试使用​​特定的数据结构,看看是否获得了很多。

使用BinaryFormatter查看MSDN示例。

答案 5 :(得分:0)

构建您自己的序列化以最小化您的结构。并序列化为二进制而不是xml。

有些事情:

internal void Save(BinaryWriter w)
{
    w.Write(this.id);
    w.Write(this.name);
    byte[] bytes = Encoding.UTF8.GetBytes(this.MyString);
    w.Write(bytes.Length);
    w.Write(bytes);

    w.Write(this.tags.Count); // nested struct/class        
    foreach (Tag tag in this.tags)
    {
        tag.Save(w);
    }
}

并有一个构建器,可以将其构建回来

public MyClass(BinaryReader reader)
{
   this.id = reader.ReadUInt32();
   etc.

}