Kirjautuminen

Haku

Tehtävät

Keskustelu: Ohjelmointikysymykset: C: Dynaaminen muistinvaraus -vinkistä

Pekka Karjalainen [17.01.2008 17:55:39]

#

Tämä on ehkä vähän pitkä sepustus koodivinkkikommentiksi, joten laitan tänne. Metabolixin vinkki ( https://www.ohjelmointiputka.net/koodivinkit/25099-c-dynaaminen-muistinvaraus-2d-taulu-rakenteet-c ) on hieman puutteellinen. Se nimittäin voi kaatua ajon aikana eräillä prosessoreilla; ei tosin kovin helposti Intelin.

Jos muutan esimerkki-koodia seuraavalla tavalla, ilmenee ongelma.

/** varaus_esimerkki.c **/

#include <stdio.h>
#include <stdlib.h>

/* Tähän väliin tarvitaan yo. koodit tai sopivat otsikkotiedostot
niistä. */
/* Tällainen include on ehdottomasti väärä ratkaisu! ;) */
#include "varaus_2d.c"

/**
* Ylimääräinen rakenne testausta varten
**/
struct kulmio {
    double a, b;
};

int main(void)
{
    const int leveys = 5, korkeus = 7;
    int vertekseja = 4, kolmioita = 4;
    int i, j;
    struct kulmio **kulmiot;

    /* Varataan, käytetään normaalisti ja vapautetaan */
    kulmiot = (struct kulmio **) calloc_2d(sizeof(struct kulmio), leveys,
korkeus);
    for (i = 0; i < leveys; ++i) {
        for (j = 0; j < korkeus; ++j) {
            kulmiot[i][j].a = i;
            kulmiot[i][j].b = j;
            printf("%f ", kulmiot[i][j].a * kulmiot[i][j].b);
        }
        printf("\n");
    }
    free_2d(kulmiot);

    return 0;
}

Tässä on tehty seuraavat muutokset: Malli-koodiin viittaukset on kaikki poistettu, koska virhe tulee calloc_2d:stä. Kulmio-tyypin alkiot on vaihdettu double-tyyppisiksi. Printf:n direktiivi on vaihdettu %f:ksi. Tiedosto varaus_2d.c on täysin sama.

Kun ohjelmaan kääntää ja ajaa, Sparc-prossulla varustettu kone ilmoittaa tylysti Bus error .

Vika on se, että calloc_2d laittaa tässä tapauksessa double-tyyppiin osoittavat osoittimet osoittamaan osoitteisiin, jotka eivät ole kahdeksalla jaollisia. Testauskoneessa osoittimen leveys on neljä tavua, ja niitä on esimerkkiajossa pariton määrä. Sparc-prossu kuitenkin vaatii, että doublen osoite on tasan kahdeksalla jaollinen. Jos se ei ole, tulee Bus error heti, kun osoitinta yritetään seurata.

Luonnollisesti taulukko, jonka leveys on parillinen, sattuu toimimaan oikein virheestä huolimatta. Esim. arvolla const int leveys = 6 on tulostus seuraava:

0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
0.000000 1.000000 2.000000 3.000000 4.000000 5.000000 6.000000
0.000000 2.000000 4.000000 6.000000 8.000000 10.000000 12.000000
0.000000 3.000000 6.000000 9.000000 12.000000 15.000000 18.000000
0.000000 4.000000 8.000000 12.000000 16.000000 20.000000 24.000000
0.000000 5.000000 10.000000 15.000000 20.000000 25.000000 30.000000

Intelin prossuilla on doublen sijoittaminen kahdeksalla jaolliseen osoitteeseen vain kiva asia eikä pakko. Se on kuitenkin oleellinen numeerisen laskennan tehokkuuden kannalta, joten alignment-ongelma olisi syytä senkin takia korjata esimerkissä. Intelin manuaaleissa kerrotaan tästä, ja sen takia gcc:ssä on optio -malign-double.

Ehdotuksena esittäisin, että osoittimien jälkeisen muistialueen pitäisi alkaa sopivasta parametrin size_t alkion_koko monikerrasta. Osoittimien ja datan välissä oleva tyhjä tila olisi silloin käyttämätöntä, mutta koska sitä tarvitaan aidosti vähemmän kuin alkion_koko -parametrin arvo, olisi hinta vain vakiomäärä muistia, eli käytännössä huomaamaton määrä isossa taulukossa.

Metabolix [18.01.2008 21:56:23]

#

Muokkasin. Tarkista toki vielä, että tein homman oikein. :)

Pekka Karjalainen [20.01.2008 21:06:22]

#

Ehkä pointterin cast int-tyyppiseksi ja sijoitus i-muuttujaan on vähän arveluttava kohta nykyisessä versiossa. Jätän harkintaasi, haluatko ottaa tästä allaolevasta mallia, vai pitää koodin nykyisen kaltaisena. Minusta esittämäni järjestys on hieman selkeämpi, ja korjauksen lasku ennen varausta voi säästää muistia useita tavuja (:-)).

Pahoittelen, jos sotkin koodisi muotoilua karseasti tehdessäni oman version. Sulutin yli-innokkaasti, koska en vain muista operaattorien järjestyksiä. Tämä kelpaa nyt sille SPARC-prossullekin edellä olleiden testien mukaan.

Tosi tarkka ihminen voisi vielä ihmetellä, mitä nollan kokoisille varauksille tapahtuu (ja mitä pitäisi). Ehkäpä voimme kuitenkin luottaa koodin soveltajan käyttävän perusjärkeä tämän asian kanssa.

/** varaus_2d.c **/

#include <stdlib.h>

/**
* Varausfunktio; calloc nollaa muistiosoiten, siksi tämäkin on calloc_2d
**/
void **calloc_2d(size_t alkion_koko, size_t d1, size_t d2)
{
    void **ret;
    size_t muistiosoite;
    size_t alabitit;
    size_t korjaus;
    int i;

    /* Lasketaan vakioiden nimiä vastaavat tiedot */
    const size_t datan_koko      = d1 * d2 * alkion_koko;
    const size_t osoitinten_koko = d1 * sizeof(void*);
    const size_t rivin_koko      = d2 * alkion_koko;

    /* Etsitään alimpien bittien maski, jota vast. bittien pitää olla nollia
    ** muistiosoitesijoitusrajoituksia vaativilla prosessoreilla. Esim. SPARC-
    ** arkkitehtuuri ei salli double-muuttujaa muualla kuin kahdeksalla
    ** jaollisesta muistiosoitepaikasta alkaen. Tämän arvon voi korvata
    ** käytetylle prosessorille sopivalla vakioarvolla erityistapauksissa. */

    alabitit = 1;
    /* haetaan alin asetettu bitti */
    while ((alkion_koko & alabitit) == 0) {
        alabitit <<= 1;
    }
    /* sitä pienemmät ovat tutkittavia eli nollattavia */
    alabitit -= 1;

    /* Lasketaan muistiosoitesijoitusrajoituksen mukainen alku datalle.
    ** Data alkaa osoittimien viemää tilaa seuraavasta muistiosoitepaikasta,
    ** jonka alabitit ovat nollia. */
    korjaus = 0;
    if (osoitinten_koko & alabitit) {
	    korjaus = (alabitit + 1) - (osoitinten_koko & alabitit);
    }

    /* Varataan koko muistiosoite yhtenä palikkana, calloc myös nollaa muistiosoiten.
    ** Lisätään korjaus, jotta mainittu asettelu onnistuu. */
    muistiosoite = (size_t) calloc(1, datan_koko + osoitinten_koko + korjaus);

    /* Osoitin tauluun on osoitin tähän muistiosoitein */
    ret = (void **) muistiosoite;

    /* Dynaaminen 2D-taulu on taulukollinen osoittimia, jotka osoittavat
    ** datariveihin. Varsinainen data alkaa osoitinten ja korjauksen jälkeen. */
    muistiosoite = muistiosoite + osoitinten_koko + korjaus;

    /* Osoittimet voidaan asettaa nyt helposti rivi kerrallaan niin,
    ** että ne osoittavat rivin koon päähän toisistaan. */
    for (i = 0; i < d1; ++i) {
        ret[i] = (void*) (muistiosoite + i * rivin_koko);
    }

    return ret;
}

/**
* Vapautusfunktio
**/
void free_2d(void *t)
{
    if (t == NULL) {
        /* Ei taulua, ei vapautusta. */
        return;
    }

    /* Koska varaus tehtiin yhtenä palasena, myös vapautus pitää tehdä niin.
    ** Tämä funktio on siis oikeastaan turha, kun normaalikin free toimii.
    ** Koodissa on kuitenkin usein selkeämpää käyttää omille erikoisuuksille
    ** omia funktioita. */
    free(t);
}

Vastaus

Aihe on jo aika vanha, joten et voi enää vastata siihen.

Tietoa sivustosta