gephi / gephi/graphstore

Store static edge weight as a primitive double field instead of a boxed Double in the attribute array.

Open
#278 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
Java
Stars
93
Forks
34
Avg merge
18h 53m
Merged PRs (30d)
13

Description

## Summary

For the default configuration (static, `Double`-typed weight), `EdgeImpl` stores the weight as a boxed `Double` in the generic `Object[] attributes`. Every edge therefore allocates a heap `Double`, and every `getWeight()` does an array read + `instanceof` + unbox. Measured overhead is ~28 bytes per edge versus ~8 for a primitive field — about 190 MB of extra heap per 10M weighted edges. Since weight is both the most frequently read edge attribute and the one carried by essentially every weighted graph, a primitive `double weight` field for the static case is a sizeable memory and GC win, and it directly serves the "low memory footprint" goal in the README.

## Environment

- `org.gephi:graphstore` `0.8.7-SNAPSHOT`, commit `5028d21`
- Build target JDK 17; measured on OpenJDK 21, compressed oops, default object alignment

## Where

The weight is written into the attribute array in the `EdgeImpl` constructor (`src/main/java/org/gephi/graph/impl/EdgeImpl.java:60`):

```java
if (graphStore == null || graphStore.configuration.getEdgeWeightType().equals(Double.class)) {
this.attributes.setAttribute(GraphStoreConfiguration.EDGE_WEIGHT_INDEX, weight); // autobox to Double
}
```

and read back, unboxed, in `getWeight()` (`EdgeImpl.java:79`):

```java
Object weightObject = attributes.getAttribute(GraphStoreConfiguration.EDGE_WEIGHT_INDEX);
if (weightObject instanceof Double) {
return (Double) weightObject; // unbox
}
```

`EDGE_WEIGHT_INDEX` is a fixed slot in the `Object[]` (`GraphStoreConfiguration.java:75`), so the value lives as a boxed `Double` object referenced from that array.

## Why it matters

Weight is read in the inner loop of weighted degree, weighted PageRank, modularity, force-directed layout, and weighted shortest paths. The boxing costs on two axes:

- **Memory / GC:** one `Double` object per edge, plus the array reference slot.
- **CPU:** an `Object[]` indirection, a type check, and an unbox on every read (on top of the synchronization tracked separately in the monitor issue).

### Measured memory (10M elements)

| Storage | Heap | Per element |
|---|---|---|
| `Object[]` of boxed `Double` (current) | ~267 MB | ~28 bytes |
| `double[]` equivalent | ~76 MB | ~8 bytes |

A primitive `double` field folds into the `EdgeImpl` object itself (~8 bytes, no separate allocation), saving on the order of 190 MB per 10M weighted edges in this configuration. Absolute numbers shift with JVM flags, but the ~3–4× ratio holds.

Measurement harness

```java
public class Mem {
static final int N = 10_000_000;
static long used(){ Runtime r = Runtime.getRuntime();
for (int i=0;i<4;i++){ System.gc(); try{Thread.sleep(80);}catch(Exception e){} }
return r.totalMemory() - r.freeMemory(); }
public static void main(String[] a){
long base = used();
Object[] boxed = new Object[N];
for (int i=0;i

## Proposed direction

Add a primitive `protected double weight` field to `EdgeImpl` as the source of truth for the **static, `Double`-typed** weight case, and route the weight-column accessors to it:

- `getWeight()` / `setWeight(double)` read and write the field directly.
- The generic column accessors for the weight column (`getAttribute(weightColumn)`, serialization in `Serialization`, and any value-index lookups) bridge to the field so the column abstraction stays consistent — i.e. the weight remains a first-class column externally, but its value is backed by a primitive internally.
- Fall back to the existing `attributes`-array path only when the weight is **dynamic** (temporal `TimeMap`) or configured to a non-`Double` type via `configuration.getEdgeWeightType()`.

This also composes with the separate attribute-monitor issue — both touch `getWeight()`, and together they make the static-weight read fully lock-free and allocation-free.

## Scope / risk

- API-visible behavior unchanged; weight stays a readable/writable column with the same semantics.
- Serialization format: needs a compatible read/write path for the weight (can keep the wire format identical by serializing the field where the boxed value used to go).
- Dynamic-weight and custom-weight-type configurations are unaffected (they keep the array path).

Contributor guide

No contributing guide indexed for this repository

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.