Saturday, February 25, 2017

SNAKE - System Design Principles to crack a system design in 5 steps


System design questions can be daunting, but with a structured approach, you can tackle them confidently. Here are five steps to help you systematically address any system design question:

1. Scenario: Understanding the Case and Interface

  • Typical Use Cases: Identify the primary use cases your system needs to support. Understand the end-user requirements and the core functionalities the system should provide.
  • Abstraction: Determine the level of abstraction your system should offer. This involves defining the system boundaries and the high-level components.
  • APIs: Design the APIs that will facilitate communication between different components of your system. Consider the inputs, outputs, and actions for each API endpoint.

2. Necessary: Constraints and Hypotheses

  • Total Users and Daily Active Users: Estimate the number of users your system will serve and the daily active user count. This helps in understanding the scale.
  • Transactions: Determine the number of transactions your system needs to handle. This includes read and write operations.
  • Concurrency: Assess the number of concurrent users or parallel executions your system must support.
  • Peak Load: Identify the peak load your system is expected to handle. This includes traffic spikes and maximum transaction rates.
  • Performance Requirements: Define the acceptable latency and response time. Understand how fast your system needs to be and the maximum delay it can tolerate.

3. Application: Services and Algorithms

  • Service Design: Break down your system into individual services or modules. Define the responsibilities and interactions of each service.
  • Algorithm Complexity: Perform a complexity analysis of the core algorithms. Understand the time and space complexity (Big O notation) to ensure efficiency.

4. Kilobit: Data Considerations

  • Data Generation: Estimate the amount of data generated by your system. This includes user-generated content, logs, and metadata.
  • Storage Requirements: Determine the storage space needed to persist the data. Plan for future growth.
  • Data Storage Solutions: Decide where to store the data. Choose between file systems, SQL databases, NoSQL databases, or a combination based on your requirements.

5. Evolve: Optimization and Scalability

  • Optimization: Identify areas for performance improvement. Optimize your system for speed, memory usage, and resource efficiency.
  • Extensibility: Ensure your system can be extended with new features and components without significant rework.
  • Scalability: Design your system to scale horizontally and vertically. Plan for adding more servers, instances, and resources as demand grows.
  • Availability: Ensure high availability of your system through redundancy, failover mechanisms, and disaster recovery plans.

4S Analysis Framework

Alternatively, you can use the 4S Analysis framework to structure your approach:

Scenario

  • Ask: Clarify the problem statement and requirements.
  • Features: Identify the core features your system needs to support.
  • QPS (Queries Per Second): Determine the expected query rate.
  • DAU (Daily Active Users): Estimate the daily active user count.
  • Interfaces: Define the interfaces for user interaction and system integration.

Service

  • Split: Break down the system into smaller, manageable services or modules.
  • Application: Design the core application logic and service responsibilities.
  • Module: Define the individual modules and their interactions.

Storage

  • Schema: Design the database schema to support your data model.
  • Data: Plan for data storage, indexing, and retrieval.
  • SQL/NoSQL/File System: Choose the appropriate storage solutions based on your data needs.

Scale

  • Sharding: Implement sharding for distributed data storage and retrieval.
  • Optimize: Optimize your system for performance and efficiency.
  • Special Case: Address special cases and edge scenarios to ensure robustness.

By following these steps and frameworks, you can systematically and confidently approach any system design question, ensuring a comprehensive and efficient solution.


Monday, February 6, 2017

Mastering Thread Synchronization in C++ with Practical Examples

Thread synchronization is crucial in multithreaded programming to ensure that multiple threads can cooperate and share resources without conflicts. Here, we explore various synchronization techniques using practical examples in C++.

1. Signaling

Signaling allows one thread to notify another that an event has occurred. This ensures that a specific section of code in one thread runs before a section in another thread.

Example: Ensuring handlerA runs before handlerB.

[code]

#include <pthread.h>
#include <semaphore.h>
#include <stdio.h>

pthread_t thread_a;
pthread_t thread_b;
sem_t sema;

sem_init(&sema, 0, 0);

void* handlerA(void*) {
    printf("functionA\n");
    sem_post(&sema);
    return NULL;
}

void* handlerB(void*) {
    sem_wait(&sema);
    printf("functionB\n");
    return NULL;
}

int main() {
    pthread_create(&thread_a, NULL, handlerA, NULL);
    pthread_create(&thread_b, NULL, handlerB, NULL);
    pthread_join(thread_a, NULL);
    pthread_join(thread_b, NULL);
    return 0;
}


2. Rendezvous:


Rendezvous extends signaling by ensuring two threads wait for each other at specific points.

Example: Ensuring steps in handlerA and handlerB occur in a specific order.

[code]
#include <pthread.h>
#include <semaphore.h>
#include <stdio.h>

pthread_t thread_a;
pthread_t thread_b;
sem_t sema;
sem_t semb;

sem_init(&sema, 0, 0);
sem_init(&semb, 0, 0);

void* handlerA(void*) {
    printf("functionA step 1\n");
    sem_post(&sema);
    sem_wait(&semb);
    printf("functionA step 2\n");
    return NULL;
}

void* handlerB(void*) {
    printf("functionB step 1\n");
    sem_post(&semb);
    sem_wait(&sema);
    printf("functionB step 2\n");
    return NULL;
}

int main() {
    pthread_create(&thread_a, NULL, handlerA, NULL);
    pthread_create(&thread_b, NULL, handlerB, NULL);
    pthread_join(thread_a, NULL);
    pthread_join(thread_b, NULL);
    return 0;
}


3. Mutex:

A mutex ensures that only one thread accesses a critical section at a time.

Example: Safely incrementing a shared counter.



[code]
#include <pthread.h>
#include <stdio.h>

pthread_t thread_a;
pthread_t thread_b;
pthread_mutex_t mutex;

int count = 0;

void* handlerA(void*) {
    pthread_mutex_lock(&mutex);
    count++;
    pthread_mutex_unlock(&mutex);
    return NULL;
}

void* handlerB(void*) {
    pthread_mutex_lock(&mutex);
    count++;
    pthread_mutex_unlock(&mutex);
    return NULL;
}

int main() {
    pthread_mutex_init(&mutex, NULL);
    pthread_create(&thread_a, NULL, handlerA, NULL);
    pthread_create(&thread_b, NULL, handlerB, NULL);
    pthread_join(thread_a, NULL);
    pthread_join(thread_b, NULL);
    pthread_mutex_destroy(&mutex);
    return 0;
}



4. Multiplex 


Multiplexing allows multiple threads to enter the critical section, controlled by a semaphore initialized to a specific value.

Example: Allowing two threads to run in the critical section simultaneously.


[code]
#include <pthread.h>
#include <semaphore.h>
#include <stdio.h>

pthread_t thread_a;
pthread_t thread_b;
pthread_t thread_c;
sem_t sema;

sem_init(&sema, 0, 2);

void* handlerA(void*) {
    for(int i = 0; i < 10; i++) {
        sem_wait(&sema);
        printf("functionA\n");
        sem_post(&sema);
    }
    return NULL;
}

void* handlerB(void*) {
    for(int i = 0; i < 10; i++) {
        sem_wait(&sema);
        printf("functionB\n");
        sem_post(&sema);
    }
    return NULL;
}

void* handlerC(void*) {
    for(int i = 0; i < 10; i++) {
        sem_wait(&sema);
        printf("functionC\n");
        sem_post(&sema);
    }
    return NULL;
}

int main() {
    pthread_create(&thread_a, NULL, handlerA, NULL);
    pthread_create(&thread_b, NULL, handlerB, NULL);
    pthread_create(&thread_c, NULL, handlerC, NULL);
    pthread_join(thread_a, NULL);
    pthread_join(thread_b, NULL);
    pthread_join(thread_c, NULL);
    return 0;
}

5. Barrier:

A barrier ensures that no thread proceeds past a certain point until all threads reach that point.

Example: Synchronizing threads before proceeding to the next step.


[code]
#include <pthread.h>
#include <semaphore.h>
#include <stdio.h>

pthread_t thread_a;
pthread_t thread_b;
pthread_t thread_c;
pthread_mutex_t mutex;
sem_t barrier;
int count = 0;

void* handlerA(void*) {
    pthread_mutex_lock(&mutex);
    count++;
    printf("Func A access done.\n");
    if (count == 3) sem_post(&barrier);
    pthread_mutex_unlock(&mutex);
    sem_wait(&barrier);
    sem_post(&barrier);
    printf("Function A\n");
    return NULL;
}

void* handlerB(void*) {
    pthread_mutex_lock(&mutex);
    count++;
    printf("Func B access done.\n");
    if (count == 3) sem_post(&barrier);
    pthread_mutex_unlock(&mutex);
    sem_wait(&barrier);
    sem_post(&barrier);
    printf("Function B\n");
    return NULL;
}

void* handlerC(void*) {
    pthread_mutex_lock(&mutex);
    count++;
    printf("Func C access done.\n");
    if (count == 3) sem_post(&barrier);
    pthread_mutex_unlock(&mutex);
    sem_wait(&barrier);
    sem_post(&barrier);
    printf("Function C\n");
    return NULL;
}

int main() {
    sem_init(&barrier, 0, 0);
    pthread_create(&thread_a, NULL, handlerA, NULL);
    pthread_create(&thread_b, NULL, handlerB, NULL);
    pthread_create(&thread_c, NULL, handlerC, NULL);
    pthread_join(thread_a, NULL);
    pthread_join(thread_b, NULL);
    pthread_join(thread_c, NULL);
    return 0;
}

6. Deadlock:

Deadlock occurs when two or more threads are waiting on each other to release resources, causing all of them to get stuck.

Example: A simple deadlock scenario.


[code]

#include <pthread.h>
#include <semaphore.h>
#include <stdio.h>

pthread_t thread_a;
pthread_t thread_b;
sem_t sema;
sem_t semb;

sem_init(&sema, 0, 0);
sem_init(&semb, 0, 0);

void* handlerA(void*) {
    printf("functionA step 1\n");
    sem_wait(&semb);
    sem_post(&sema);
    printf("functionA step 2\n");
    return NULL;
}

void* handlerB(void*) {
    printf("functionB step 1\n");
    sem_wait(&sema);
    sem_post(&semb);
    printf("functionB step 2\n");
    return NULL;
}

int main() {
    pthread_create(&thread_a, NULL, handlerA, NULL);
    pthread_create(&thread_b, NULL, handlerB, NULL);
    pthread_join(thread_a, NULL);
    pthread_join(thread_b, NULL);
    return 0;
}

7. Producer-consumer:

The producer-consumer problem involves multiple threads producing items and adding them to a buffer, while other threads consume and remove them from the buffer.

Example: Unlimited buffer producer-consumer.


[code]

#include <pthread.h>
#include <semaphore.h>
#include <deque>
#include <stdio.h>

pthread_t thread_producer;
pthread_t thread_consumer;
pthread_mutex_t mutex;
sem_t sema;
std::deque<int> buffer;

pthread_mutex_init(&mutex, NULL);
sem_init(&sema, 0, 0);

void* producer(void*) {
    while(true) {
        int num = rand() % 100;
        printf("producer set %d\t", num);
        pthread_mutex_lock(&mutex);
        buffer.push_front(num);
        sem_post(&sema);
        pthread_mutex_unlock(&mutex);
    }
    return NULL;
}

void* consumer(void*) {
    while(true) {
        sem_wait(&sema);
        pthread_mutex_lock(&mutex);
        int back = buffer.back();
        buffer.pop_back();
        pthread_mutex_unlock(&mutex);
        printf("consumer get %d\n", back);
    }
    return NULL;
}

int main() {
    pthread_create(&thread_producer, NULL, producer, NULL);
    pthread_create(&thread_consumer, NULL, consumer, NULL);
    pthread_join(thread_producer, NULL);
    pthread_join(thread_consumer, NULL);
    return 0;
}

8. Finite Buffer Producer-consumer:

When the buffer is finite, producers need to wait if the buffer is full, and consumers need to wait if the buffer is empty.

Example: Producer-consumer with finite buffer.

[code]
#include <pthread.h> #include <semaphore.h> #include <deque> #include <stdio.h> #define N 10 pthread_t thread_producer; pthread_t thread_consumer; pthread_mutex_t mutex; sem_t sema; sem_t spaces; std::deque<int> buffer; pthread_mutex_init(&mutex, NULL); sem_init(&sema, 0, 0); sem_init(&spaces, 0, N); void* producer(void*) { while(true) { int num = rand() % 100; printf("producer set %d\t", num); sem_wait(&spaces); // wait signal pthread_mutex_lock(&mutex); buffer.push_front(num); sem_post(&sema); pthread_mutex_unlock(&mutex); } return NULL; } void* consumer(void*) { while(true) { sem_wait(&sema); pthread_mutex_lock(&mutex); int back = buffer.back(); buffer.pop_back(); pthread_mutex_unlock(&mutex); sem_post(&spaces); // signaling producer printf("consumer get %d\n", back); } return NULL; } int main() { pthread_create(&thread_producer, NULL, producer, NULL); pthread_create(&thread_consumer, NULL, consumer, NULL); pthread_join(thread_producer, NULL); pthread_join(thread_consumer, NULL); return 0; }

9. Readers and Writers Problem:

This problem involves readers and writers who need different synchronization. Multiple readers can access the resource simultaneously, but a writer requires exclusive access.

Example: Readers-writers problem.

[code]
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>

#define N 4
pthread_t thread_writer[N];
pthread_t thread_reader[N];
int readers = 0;

pthread_mutex_t writeOk;
pthread_mutex_t readerOn;

char BufferA[20];
char BufferB[20];

void* writer(void*) {
    int counter = 1;
    while(1){
        pthread_mutex_lock(&writeOk);
        sprintf(BufferA, "WriteA: %d", counter);
        usleep(100);
        sprintf(BufferB, "WriteB: %d", counter);
        counter++;
        pthread_mutex_unlock(&writeOk);
    }
    return NULL;
}

void* reader(void* arg) {
    while(1) {
        pthread_mutex_lock(&readerOn);
        readers++;
        if (readers == 1) {
            pthread_mutex_lock(&writeOk);
        }
        pthread_mutex_unlock(&readerOn);

        printf("%s from thread %ld\n", BufferA, (long)arg);
        printf("%s from thread %ld\n", BufferB, (long)arg);
        usleep(500);

        pthread_mutex_lock(&readerOn);
        readers--;
        if(readers == 0) {
            pthread_mutex_unlock(&writeOk);
        }
        pthread_mutex_unlock(&readerOn);
    }
    return NULL;
}

int main() {
    pthread_mutex_init(&writeOk, NULL);
    pthread_mutex_init(&readerOn, NULL);

    for(int i = 0; i < N; i++) {
        pthread_create(&thread_writer[i], NULL, writer, NULL);
        pthread_create(&thread_reader[i], NULL, reader, (void*)(long)i);
    }

    for(int i = 0; i < N; i++) {
        pthread_join(thread_writer[i], NULL);
        pthread_join(thread_reader[i], NULL);
    }

    pthread_mutex_destroy(&writeOk);
    pthread_mutex_destroy(&readerOn);

    return 0;
}
By understanding and implementing these synchronization techniques, you can effectively manage multithreaded programs, ensuring safe and efficient execution of concurrent tasks.


Wednesday, February 1, 2017

C++ Virtual Table Example


C++ Vtable Example
Revised 10 September 1999


[990910 IBM -- Brian] Added more examples, split out the two kinds of adjustments in Table 1a, and added a summary of the component counts for the two approaches.


Table 1a: Example Code and Call Semantics 
Declarations Call Callee Call-site
Adjustment 
Thunk/Entry-point
Adjustment
struct A {
  virtual void f ();
  virtual void g ();
  virtual void h ();
  int ia;
};

A *pa;
pa->f()A::f() none none
pa->g()A::g() none none
pa->h()A::h()nonenone
struct B: public virtual A {
  void f ();
  void h ();
  int ib;
};

B *pb;
A *pa_in_b = pb;
pb->f()B::f() none none
pb->A::f()A::f() B => Anone
pb->g()A::g() B => A none
pb->h()B::h()nonenone
pa_in_b->f()B::f() none A => B
pa_in_b->g()A::g() none none
pa_in_b->h()B::h() none A => B
pa_in_b->A::f()A::f() none none
struct C: public virtual A {
  void g ();
  void h ();
  int ic;
};

C *pc;
A *pa_in_c = pc;
pc->f()A::f() C => A none
pc->g()C::g() none none
pc->A::g()A::g() C => A none
pc->h()C::h() none none
pa_in_c->f()A::f() none none
pa_in_c->g()C::g() noneA => C
pa_in_c->h()C::h() noneA => C
pa_in_c->A::g()A::g() none none
struct D: public B, public C {
  int id;
  void h();
};

D *pd;


A *pa_in_d = pd;
B *pb_in_d = pd;
C *pc_in_d = pd;


A *pa_in_b_in_d = pb_in_d;
A *pa_in_c_in_d = pc_in_d;
pd->f()B::f() none [D => B] none
pd->g()C::g() D => C none
pd->h()D::h() none none
pa_in_d->f()B::f() none A => B
pa_in_d->g()C::g() noneA => C
pa_in_d->h()D::h() noneA => D
pb_in_d->f()B::f() none none
pb_in_d->g()C::g() B => AA => C
pb_in_d->h()D::h() none B => D
pc_in_d->f()B::f() C => AA => B
pc_in_d->g()C::g() none none
pc_in_d->h()D::h() none C => D
pa_in_b_in_d->f()same as for pa_in_d 
pa_in_b_in_d->g()
pa_in_b_in_d->h()
pa_in_c_in_d->f()
pa_in_c_in_d->g()
pa_in_c_in_d->h()
p...d->A::f()A::f() ... => A none
p...d->A::g()A::g() ... => A none
p...d->A::h()A::g() ... => A none
struct X {
  int ix;
  virtual void x();
};
struct E : X, D {
  int ie;
  void f();
  void h();
};
pe->f()E::f()nonenone
pe->g()C::g()E => Cnone
pe->h()E::h()none none
pe->x()X::x()none [E=>X]none
pa_in_e->f()
E::f()noneA => E
pa_in_e->g()C::g()noneA => C
pa_in_e->h()E::h()noneA => E
pb_in_e->f()E::f()noneB => E
pb_in_e->g()C::g()B => AA => C
pb_in_e->h()E::h()none B => E
pc_in_e->f()E::f()C => AA => E
pc_in_e->g()C::g()nonenone
pc_in_e->h()E::h()noneC => E
pd_in_e->f()E::f()none [D=>B]B => E
pd_in_e->g()C::g()D => Cnone
pd_in_e->h()E::h()noneD => E

Table 1b: Example Data Layout 
Declarations Size OffsetMember 
struct A {
  virtual void f ();
  virtual void g ();
  virtual void h ();
  int ia;
};
16 0A::vptr
ia
struct B: public virtual A {
  void f ();
  void h ();
  int ib;
};
32 0B::vptr
ib
16 A::vptr
24 ia
struct C: public virtual A {
  void g ();
  void h ();
  int ic;
};
32 0C::vptr
ic
16 A::vptr
24 ia
struct D: public B, public C {
  void h ();
  int id;
};
48 0D/B::vptr
ib
16 C::vptr
24 ic
28 id
32 A::vptr
40 ia
struct X {
  int ix;
  virtual void x();
};
struct E : X, D {
  void f ();
  void h ();
  int ie;
};
640X/E::vptr
8ix
16D/B::vptr
24ib
32C::vptr
40ic
48id
56A::vptr
64ia

Table 1c: Example Vtable Layout 
Declarations Vtable (HP) 1,2,3Vtable (Cygnus/IBM)
struct A {
  virtual void f ();
  virtual void g ();
  virtual void h ();
  int ia;
};
A::offset_to_top (0)
A::rtti
-- A vtable address --
A::f() []
A::g() []
A::h() []
A::offset_to_top (0)
A::rtti
-- A vtable address --
A::f() []
A::g() []
A::h() []
struct B: public virtual A {
  void f ();
  void h ();
  int ib;
};
B::offset_to_A (16)
B::offset_to_top (0)
B::rtti
-- B vtable address --
B::f() []
B::h() []

A::offset_to_top (-16)
A::rtti
-- A-in-B vtable address --
B::f() [[-72] B::offset_to_A : thunk]
A::g() []
B::h() [[-72] B::offset_to_A : thunk]
B::offset_to_A (16)
B::offset_to_top (0)
B::rtti
-- B vtable address --
B::f() []
B::h() []

A::offset_for_h (-16)
A::offset_for_g (0)
A::offset_for_f (-16)
A::offset_to_top (-16)
A::rtti
-- A-in-B vtable address --
B::f() [[-24]offset_for_f]
A::g() []
B::h() [[-40]offset_for_h]
struct C: public virtual A {
  void g ();
  void h ();
  int ic;
};
C::offset_to_A (16)
C::offset_to_top (0)
C::rtti
-- C vtable address --
C::g() []
C::h() []

A::offset_to_top (-16)
A::rtti
-- A-in-C vtable address --
A::f() []
C::g() [[-72] C::offset_to_A : thunk]
C::h() [[-72] C::offset_to_A : thunk]
total size 15*8 = 120 bytes
C::offset_to_A (16)
C::offset_to_top (0)
C::rtti
C vtable address --
C::g() []
C::h() []

A::offset_for_h (-16)
A::offset_for_g (-16)
A::offset_for_f (0)
A::offset_to_top (-16)
A::rtti
A-in-C vtable address --
A::f() []
C::g() [[-32] offset_for_g]
C::h() [[-40] offset_for_h]
total size 18*8 = 144 bytes
struct D: public B, public C {
  void h ();
  int id;
};
D::offset_to_C (16)
D::offset_to_A (32)
D::offset_to_top (0)
D::rtti
-- D, B-in-D vtable address --
B::f() []
D::h() []

C::offset_to_A (16)
C::offset_to_top (-16)
C::rtti
-- C-in-D vtable address --
C::g() []
D::h() [[-88] D::offset_to_C]

A::offset_to_top (-32)
A::rtti
-- A-in-D vtable address --
B::f() [[-128] D::offset_to_A : thunk]
C::g() [[-72] C::offset_to_A : thunk]
D::h() [[-128] D::offset_to_A : thunk]

total size 23*8 = 184 bytes
D::offset_to_A (32)
D::offset_to_top (0)
D::rtti
-- D, B-in-D vtable address --
B::f() []
D::h() []

C::offset_to_A (16)
C::offset_to_top (-16)
C::rtti
-- C-in-D vtable address --
C::g() []
D::h() [-16]

A::offset_for_h (-32)
A::offset_for_g (-16)
A::offset_for_f (-32)
A::offset_to_top (-32)
A::rtti
-- A-in-D vtable address --
B::f() [[-24] offset_for_f]
C::g() [[-32] offset_for_g]
D::h() [[-40] offset_for_h]
total size 25*8 = 200 bytes
struct X {
  int ix;
  virtual void x();
};
struct E : X, D {
  int ie;
  void f();
  void h ();
};
E::offset_to_D (16)
not used 
not used
not used
not used
E::offset_to_C (32)
E::offset_to_A (56)
E::offset_to_top (0)
E::rtti
-- E, X-in-E vtable address --
X::x() []
E::f() []
E::h() []

D::offset_to_A (40)
D::offset_to_top (-16)
D::rtti
-- D, B-in-E vtable address --
E::f() [[-144] E::offset_to_D]
E::h() [[-144] E::offset_to_D]

C::offset_to_A (24)
C::offset_to_top (-32)
C::rtti
-- C-in-E vtable address --
C::g() []
E::h() [[-144] E::offset_to_C]

A::offset_to_top (-56)
A::rtti
-- A-in-E vtable address --
E::f() [[-200] E::offset_to_A : thunk]
C::g() [[-72] C::offset_to_A : thunk]
E::h() [[-200] E::offset_to_A : thunk]
total size 37*8 = 296 bytes
E::offset_to_A (56)
E::offset_to_top (0)
E::rtti
-- E, X-in-E vtable address --
X::x() []
E::f() []
E::h() []

D::offset_to_A (40)
D::offset_to_top (-16)
D::rtti
-- D, B-in-E vtable address --
E::f() [-16]
E::h() [-16]

C::offset_to_A (24)
C::offset_to_top (-32)
C::rtti
-- C-in-E vtable address --
C::g() []
E::h() [-32]

A::offset_for_h (-56)
A::offset_for_g (-24)
A::offset_for_f (-56)
A::offset_to_top (-56)
A::rtti
-- A-in-E vtable address --
E::f() [[-24] A::offset_for_f ]
C::g() [[-32] A::offset_for_g ]
E::h() [[-40] A::offset_for_h ]
total size 34*8 = 272 bytes
  1. Numbers in parentheses after offset_to_top entries are actual values.
  2. Class prefixes for functions identify class where defined.
  3. Information in square brackets after function pointer entries indicates entry-point adjustment:
    [] no adjustment required, use primary entry point
    [n] use adjusting entry point that adds "n" to this[[n] blurb]  use adjusting entry point that dereferences vptr+n and subtracts (HP) or adds (Cygnus/IBM)
        that value to this. blurb is the name of the accessed  field
    [[n] blub : thunk]  use adjusting 3rd party thunk that dereferences vptr+n and subtracts that value from this
Notes: 1) Each function descriptor in the vtable is 16 bytes but the offset and data pointers are only 8, the earlier versions of this table didn't take that into account
2) In the HP column for struct E, I have omitted the D::offset_to_C field because the overrides in E render it unnecessary.  However, if maintaining navigability inside the nonvirtual parts of the vtable is important then this "cleanup" can only be done for direct nonvirtual bases and not for more deeply nested ones.
3) I have taken Christophe at his word that thunks are used for adjusting vtable entries in virtual bases in the HP proposal. Some of them could be done with entry points though.
When all is said and done we have

x/y/z
x = # direct secondary entries
y = # "reach back" secondary entries
z = # 3rd-party thunks
FunctionHPCygnus/IBM
A::f0/0/00/0/0
A::g0/0/00/0/0
A::h0/0/00/0/0
B::f0/0/20/1/0
B::h0/0/10/1/0
C::g0/0/10/1/0
C::h0/0/10/1/0
D::h0/1/11/1/0
E::f0/1/11/1/0
E::h0/1/12/1/0